2013-08-05 36 views
6

当我使用TObjectDictionary,其中TKey是对象时,我的应用程序工作不正确。 我有两个单位,即包含两个类。第一单元:使用对象作为TObjectDictionary中的键

unit RubTerm; 

interface 

type 
    TRubTerm = Class(TObject) 
    private 
    FRubricName: String; 
    FTermName: String; 
    public 
    property RubricName: String read FRubricName; 
    property TermName: String read FTermName; 
    constructor Create(ARubricName, ATermName: String); 
    end; 

implementation 

constructor TRubTerm.Create(ARubricName, ATermName: String); 
begin 
    Self.FRubricName := ARubricName; 
    Self.FTermName := ATermName; 
end; 

end; 

而第二单元:

unit ClassificationMatrix; 

interface 

uses 
    System.Generics.Collections, System.Generics.Defaults, System.SysUtils, RubTerm; 

type 
TClassificationMatrix = class(TObject) 
    private 
    FTable: TObjectDictionary<TRubTerm, Integer>; 
    public 
    constructor Create; 
    procedure TClassificationMatrix.AddCount(ADocsCount: Integer; ARubName, ATermName: String); 
    function TClassificationMatrix.GetCount(ARubName, ATermName: String): Integer; 
    end; 

implementation 

constructor TClassificationMatrix.Create; 
begin 
    FTable := TObjectDictionary<TRubTerm, Integer>.Create; 
end; 

procedure TClassificationMatrix.AddCount(ADocsCount: Integer; ARubName, ATermName: String); 
var 
    ARubTerm: TRubTerm; 
begin 
    ARubTerm := TRubTerm.Create(ARubName, ATermName); 
    FTable.Add(ARubTerm, ADocsCount); 
end; 

function TClassificationMatrix.GetCount(ARubName, ATermName: String): Integer; 
var 
    ARubTerm: TRubTerm; 
begin 
    ARubTerm := TRubTerm.Create(ARubName, ATermName); 
    FTable.TryGetValue(ARubTerm, Result); 
end; 

end; 

但代码工作非正常的这个片段:

procedure TestTClassificationMatrix.TestGetCount; 
var 
    DocsCountTest: Integer; 
begin 
    FClassificationMatrix.AddCount(10, 'R', 'T'); 
    DocsCountTest := FClassificationMatrix.GetCount('R', 'T'); 
end; 
// DocsCountTest = 0! Why not 10? Where is problem? 

谢谢!

+1

你必须添加一个相等比较器让字典知道,你是什么意思的平等。否则,密钥索引建立在实例参考 –

回答

3

字典取决于一个关键值。您正在存储对密钥中的对象的引用。如果创建两个设置完全相同的对象,则具有不同的值,因此具有不同的键。

var 
    ARubTerm1: TRubTerm; 
    ARubTerm2: TRubTerm; 
begin 
    ARubTerm1 := TRubTerm.Create('1', '1'); 
    ARubTerm2 := TRubTerm.Create('1', '1'); 
// ARubTerm1 = ARubTerm2 is not possible here as ARubTerm1 points to a different address than ARubTerm2 
end; 

相反,您可以使用字符串作为基于RubricName和TermName的TObjectDictonary中的第一个类型参数。有了这个,你会得到相同的价值。

还应该注意的是,XE2中的上述代码会产生两个内存泄漏。每个创建的对象都必须被释放。因此这部分代码也在泄漏内存

function TClassificationMatrix.GetCount(ARubName, ATermName: String): Integer; 
var 
    ARubTerm: TRubTerm; 
begin 
    ARubTerm := TRubTerm.Create(ARubName, ATermName); 
    FTable.TryGetValue(ARubTerm, Result); 
end; 

考虑到所有这一切。如果你想使用一个对象作为一个键,你可以使用自定义等式比较器来完成。这里是你的例子改为执行IEqualityComparer<T>,并修复一些内存泄漏。

unit ClassificationMatrix; 

interface 

uses 
    Generics.Collections, Generics.Defaults, SysUtils, RubTerm; 

type 
TClassificationMatrix = class(TObject) 
    private 
    FTable: TObjectDictionary<TRubTerm, Integer>; 
    public 
    constructor Create; 
    procedure AddCount(ADocsCount: Integer; ARubName, ATermName: String); 
    function GetCount(ARubName, ATermName: String): Integer; 
    end; 

implementation 

constructor TClassificationMatrix.Create; 
var 
Comparer : IEqualityComparer<RubTerm.TRubTerm>; 
begin 
    Comparer := TRubTermComparer.Create; 
    FTable := TObjectDictionary<TRubTerm, Integer>.Create([doOwnsKeys],TRubTermComparer.Create); 
end; 

procedure TClassificationMatrix.AddCount(ADocsCount: Integer; ARubName, ATermName: String); 
var 
    ARubTerm: TRubTerm; 
begin 
    ARubTerm := TRubTerm.Create(ARubName, ATermName); 
    FTable.Add(ARubTerm, ADocsCount); 
end; 

function TClassificationMatrix.GetCount(ARubName, ATermName: String): Integer; 
var 
    ARubTerm: TRubTerm; 
begin 
    ARubTerm := TRubTerm.Create(ARubName, ATermName); 
    try 
    if Not FTable.TryGetValue(ARubTerm, Result) then 
     result := 0; 
    finally 
    ARubTerm.Free; 
    end; 
end; 

end. 

而且RubTerm.pas单元

unit RubTerm; 

interface 
uses Generics.Defaults; 

type 
    TRubTerm = Class(TObject) 
    private 
    FRubricName: String; 
    FTermName: String; 
    public 
    property RubricName: String read FRubricName; 
    property TermName: String read FTermName; 
    constructor Create(ARubricName, ATermName: String); 
    function GetHashCode: Integer; override; 
    end; 

    TRubTermComparer = class(TInterfacedObject, IEqualityComparer<TRubTerm>) 
    public 
    function Equals(const Left, Right: TRubTerm): Boolean; 
    function GetHashCode(const Value: TRubTerm): Integer; 
    end; 


implementation 

constructor TRubTerm.Create(ARubricName, ATermName: String); 
begin 
    Self.FRubricName := ARubricName; 
    Self.FTermName := ATermName; 
end; 


{ TRubTermComparer } 

function TRubTermComparer.Equals(const Left, Right: TRubTerm): Boolean; 
begin 
    result := (Left.RubricName = Right.RubricName) and (Left.TermName = Right.TermName); 
end; 

function TRubTermComparer.GetHashCode(const Value: TRubTerm): Integer; 
begin 
    result := Value.GetHashCode; 
end; 

//The Hashing code was taken from David's Answer to make this a complete answer.  
{$IFOPT Q+} 
    {$DEFINE OverflowChecksEnabled} 
    {$Q-} 
{$ENDIF} 
function CombinedHash(const Values: array of Integer): Integer; 
var 
    Value: Integer; 
begin 
    Result := 17; 
    for Value in Values do begin 
    Result := Result*37 + Value; 
    end; 
end; 
{$IFDEF OverflowChecksEnabled} 
    {$Q+} 
{$ENDIF} 

function GetHashCodeString(const Value: string): Integer; 
begin 
    Result := BobJenkinsHash(PChar(Value)^, SizeOf(Char) * Length(Value), 0); 
end; 

function TRubTerm.GetHashCode: Integer; 

begin 
    Result := CombinedHash([GetHashCodeString(Value.RubricName), 
    GetHashCodeString(Value.TermName)]);  
end; 

end. 
+2

上。这在一般情况下是广义的,但是在具体情况中严重错误。您不能连接两个字符串并比较结果。如果你这样做,那么你有'一个',''='','一个'例如。你需要比较两个字段。散列的方法相同。而且你不能通过使用散列来实现equals。不同的散列意味着不同的值。但是相同的散列并不意味着相同的值。有更多的值存在散列,所以这显然是一个错误的假设。 –

+0

更新了我的Equals实现,但只保留哈希,因为它在答案中覆盖得更好。 –

+0

如果你不打算修复哈希码(并且我没有看到你不应该修复它的原因,并且可以随意从我的答案中复制代码,如果你愿意的话),你至少应该在编辑中清楚说明它被打破。 –

7

的根本问题,这里是你的类型的默认相等比较器的行为不想要的方式运行。你想要的平等意味着值相等,但默认比较给出参考相等

事实上,你希望值相等是一个强烈的迹象表明,你应该使用值类型而不是引用类型。这是我建议的第一个改变。

type 
    TRubTerm = record 
    RubricName: string; 
    TermName: string; 
    class function New(const RubricName, TermName: string): TRubTerm; static; 
    class operator Equal(const A, B: TRubTerm): Boolean; 
    class operator NotEqual(const A, B: TRubTerm): Boolean; 
    end; 

class function TRubTerm.New(const RubricName, TermName: string): TRubTerm; 
begin 
    Result.RubricName := RubricName; 
    Result.TermName := TermName; 
end; 

class operator TRubTerm.Equal(const A, B: TRubTerm): Boolean; 
begin 
    Result := (A.RubricName=B.RubricName) and (A.TermName=B.TermName); 
end; 

class operator TRubTerm.NotEqual(const A, B: TRubTerm): Boolean; 
begin 
    Result := not (A=B); 
end; 

我添加TRubTerm.New作为辅助方法可以很容易地初始化记录的新实例。为了方便起见,您可能会发现如上所述,重载等式和不等式运算符会很有用。

一旦你切换到一个值类型,那么你也会改变字典来匹配。使用TDictionary<TRubTerm, Integer>而不是TObjectDictionary<TRubTerm, Integer>。切换到一个值类型也将有利于修复现有代码中的所有内存泄漏。您现有的代码会创建对象,但不会破坏它们。

这会让你回家的路上,但你仍然需要为你的字典定义一个相等比较器。记录的默认比较器将基于引用相等性,因为字符串(尽管用作值类型)存储为引用。

为了让你需要实现以下比较函数,其中TTRubTerm更换合适的相等比较:

TEqualityComparison<T> = reference to function(const Left, Right: T): Boolean; 
THasher<T> = reference to function(const Value: T): Integer; 

我会实现创纪录的这些作为静态类的方法。

type 
    TRubTerm = record 
    RubricName: string; 
    TermName: string; 
    class function New(const RubricName, TermName: string): TRubTerm; static; 
    class function EqualityComparison(const Left, 
     Right: TRubTerm): Boolean; static; 
    class function Hasher(const Value: TRubTerm): Integer; static; 
    class operator Equal(const A, B: TRubTerm): Boolean; 
    class operator NotEqual(const A, B: TRubTerm): Boolean; 
    end; 

实施EqualityComparison是很容易的:

class function TRubTerm.EqualityComparison(const Left, Right: TRubTerm): Boolean; 
begin 
    Result := Left=Right; 
end; 

但散列器,需要多一点思考。您需要分别对每个字段进行散列,然后将散列组合起来。供参考:

的代码看起来是这样的:

{$IFOPT Q+} 
    {$DEFINE OverflowChecksEnabled} 
    {$Q-} 
{$ENDIF} 
function CombinedHash(const Values: array of Integer): Integer; 
var 
    Value: Integer; 
begin 
    Result := 17; 
    for Value in Values do begin 
    Result := Result*37 + Value; 
    end; 
end; 
{$IFDEF OverflowChecksEnabled} 
    {$Q+} 
{$ENDIF} 

function GetHashCodeString(const Value: string): Integer; 
begin 
    Result := BobJenkinsHash(PChar(Value)^, SizeOf(Char) * Length(Value), 0); 
end; 

class function TRubTerm.Hasher(const Value: TRubTerm): Integer; 
begin 
    Result := CombinedHash([GetHashCodeString(Value.RubricName), 
    GetHashCodeString(Value.TermName)]); 
end; 

最后,当你实例化你的字典,你需要提供一个IEqualityComparison<TRubTerm>。像这样实例化你的字典:

Dict := TDictionary<TRubTerm,Integer>.Create(
    TEqualityComparer<TRubTerm>.Construct(
    TRubTerm.EqualityComparison, 
    TRubTerm.Hasher 
) 
); 
+1

一如既往的出色工作。但作为一个旁观,退后一秒,与其他一些语言相比,这看起来不是很多工作吗?您是否想过简化这一部分的基础工作将与XE5和nextgen编译器一起提供?值类型与引用类型,对象与Delphi的只是因为我们没有内存管理记录与方法,TDictionary与TObjectDictionary,11个不同的比较类...它似乎只是语言膨胀不受控制地解决许多从未解决的缺陷。和“BobJenkinsHash”而不是“哈希”? :-( – alcalde

+1

@alcade我同意语法太笨拙和冗长。 –