【问题标题】:Use objects as keys in TObjectDictionary在 TObjectDictionary 中使用对象作为键
【发布时间】:2013-08-06 19:09:51
【问题描述】:

当我使用 TObjectDictionary(其中 TKey 是对象)时,我的应用程序无法正常工作。 我有两个单元,那包含两个类。第一单元:

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?

谢谢!

【问题讨论】:

  • 你必须添加一个相等比较器才能让字典知道,你所说的相等是什么意思。否则键索引建立在实例引用上

标签: delphi delphi-xe2


【解决方案1】:

这里的根本问题是您的类型的默认相等比较器的行为方式与您希望的不同。您希望相等表示值相等,但默认比较给出引用相等

您希望值相等这一事实强烈表明您应该使用值类型而不是引用类型。这是我建议的第一个更改。

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&lt;TRubTerm, Integer&gt; 而不是TObjectDictionary&lt;TRubTerm, Integer&gt;。切换到值类型还有助于修复现有代码中的所有内存泄漏。您现有的代码会创建对象但从不销毁它们。

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

要制作合适的相等比较器,您需要实现以下比较函数,其中T 替换为TRubTerm

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&lt;TRubTerm&gt;。像这样实例化你的字典:

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

【讨论】:

  • 一如既往的出色工作。但顺便说一句,退后一步,与其他一些语言相比,这似乎不是很多工作吗?您是否认为 XE5 和 nextgen 编译器会为简化这些工作奠定基础?值类型与引用类型、对象与 Delphi 的 just-because-we-don't-have-memory-management 方法记录、TDictionary 与 TObjectDictionary、11 个不同的比较器类……看起来语言正在膨胀失控以解决许多从未解决的缺陷。和“BobJenkinsHash”而不是“哈希”? :-(
  • @alcade 我同意语法太笨拙和冗长。
【解决方案2】:

字典依赖于键值。您正在密钥中存储对对象的引用。如果您创建两个设置相同的对象,则它们具有不同的值,因此具有不同的键。

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&lt;T&gt;,并修复了一些内存泄漏。

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.

【讨论】:

  • 这在总体上大体上是合理的,但在细节上却大错特错。您不能连接两个字符串并比较结果。如果你这样做,那么你有'a',''='','a'例如。您需要比较这两个字段。哈希的方法相同。而且你不能通过使用哈希来实现equals。不同的哈希意味着不同的值。但是相等的散列并不意味着相等的值。有更多的值有哈希,所以这显然是一个错误的假设。
  • 更新了我的 Equals 实现,但保留了 Hashing,因为它在您的答案中覆盖得更好。
  • 如果您不打算修复哈希码(我认为您没有理由不修复它,如果您愿意,请随时从我的答案中复制代码),您至少应该在编辑中明确说明它已损坏。
  • 好的,我从你关于散列的答案中获取了代码。未经许可,我从不擅长这样做。
  • @RobertLove 这是一个漂亮、清晰的英文书面解释,说明了正在发生的事情以及如何解决它。说真的 - 教科书质量(现在正在学习一门新语言,所以我非常熟悉书籍是如何解释这些东西的)。
猜你喜欢
  • 2015-05-13
  • 1970-01-01
  • 2022-01-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-08-29
  • 2015-03-26
相关资源
最近更新 更多