【问题标题】:Equals and GetHashCode for TDictionary<TVehicle, TPerson>Dictionary<Vehicle, TO Person> 的 Equals 和 GetHashCode
【发布时间】:2011-03-15 17:24:45
【问题描述】:

如果我在 Delphi 中使用 TDictionary 实现关系 Car Owner,我应该如何实现 IEqualityComparer 的 Equals 和 GetHashCode 函数? (GetHashCode 返回一个 Integer,用于 TDictionary 中的散列。)

对于 TVehicle 类,假设它有一个 VIN(车辆识别号)。

我应该如何实现 VIN 的哈希码?

更新:在这个例子中,对象标识不是指“两个对象指针的内存位置的标识”,而是“同一对象的两个实例的标识,基于唯一且不变的(“不可变") 其属性的组合'。

因此,我不需要通过地图中的内存地址搜索车辆,而是需要具有我正在寻找的 id 的车辆。

考虑一个包含车主数据的数据库,在应用程序启动时加载到字典中。现在,如果用户在申请表中输入 VIN,应用程序如何在字典中找到车辆?如果代码使用VehicleFactory.CreateVehicleFromDatabase(Edit1.Text); 创建一个新实例并在字典中搜索此对象,则 Equals 的默认实现不会在映射中找到任何条目,因为它会查找内存地址。为了找到车辆,Equals 需要比较 VIN。

所以我必须创建一个自定义的 IEqualityComparer。实现 Equals 是微不足道的。但是 GetHashCode 呢?对于字符串属性,我不能简单地使用字符串的地址(请参阅Are Delphi strings immutable? 中的 Berry Kelly:“如果您从两个单独的代码部分创建相同的字符串,它们将不会共享相同的后备存储”),所以字符串属性的 GetHashCode 函数需要自定义实现。

我还发现我发现了问题How do I hash a string with Delphi? - 有一个示例包含HashValue('Hello World')

【问题讨论】:

  • 字符串的默认比较器不比较地址。
  • 拼图中缺少的部分或多或少是字符串的 GetHashCode。在 Delphi 2009 及更高版本中,所有对象都有一个 GetHashCode 方法。字符串不是对象,所以我希望在较新版本的 Delphi 中有一个系统函数。
  • 字符串确实有哈希码,它不是基于字符串的地址,而是基于它的内容。你是从哪里知道在 Delphi 中不能对字符串进行哈希处理的?
  • 默认字符串哈希如下所示:BobJenkinsHash(Value[1], Length(Value) * SizeOf(Value[1]), 0) 其中Valuestring。它正在比较数据。
  • @David 如果这是一个答案,我会接受它wink

标签: delphi generics hashmap tdictionary


【解决方案1】:

您似乎被误导相信 Delphi 字符串不附带默认的哈希码实现。

事实并非如此。当您使用字符串值作为键创建TDictionary 时,将根据字符串的内容计算哈希。如果Value 是一个字符串变量,那么代码如下所示:

BobJenkinsHash(Value[1], Length(Value) * SizeOf(Value[1]), 0);

我认为这回答了您关于字符串哈希的部分问题。


其他答案的 cmets 以及我已删除的答案是关于您正在考虑的设计问题的有趣讨论。我仍然怀疑您是否认为正确的解决方案是允许 TVehicle 实例和 VIN 之间的多对一关系。

您已确认您不能有多个具有相同 VIN 但数据不同的 TVehicle 实例。在我看来,实现这一目标的最佳方法是确保 TVehicle 实例和 VIN 之间存在一对一的关系。

这种一对一的关系很容易实现。您需要使 TVehicle 实例的实例化成为工厂类的私有函数。这个工厂类包含一个包含现有车辆实例的字典,TDictionary&lt;string,TVehicle&gt;。如果您需要获取车辆,请向工厂索取。它要么返回位于其字典中的现有字典,要么合成一个新字典。

毫无疑问,还有许多其他方法可以实现这种效果,但我强烈建议您考虑一种方法,即每个 VIN 只产生一个车辆实例。

【讨论】:

  • 对于 UnicodeString,这将是 Generics.Defaults.pas 中的函数 GetHashCode_UString(我担心这会用编译器魔法来实现)。这是缺少的部分,谢谢!
【解决方案2】:

如果可能的话,我会将 KISS 原则用于这一点。如果您的实际密钥是 ID 而不是车辆本身,那么为什么不使用 TDictionary&lt;string, TPerson&gt; 而不是 TDictionary&lt;TVehicle, TPerson&gt;?这样您就不必担心自定义比较器了。

【讨论】:

  • 然后我需要两本字典,一本也用于车辆。这意味着我必须同步这两个字典。并且 TDictionary 更具自我记录性。
  • @mjn 既然知道了它的 VIN 就可以合成一个 TVehicle,为什么需要保留任何 TVehicle 实例?梅森说的很对。
  • 如果字典键没有由一个属性组成的 id 而是复合值,这也不起作用
  • @David 我需要 TVehicle 实例作为字典中的键,它们与所有实例属性集一起存储在其中(例如来自数据库)。如果我仅使用 VIN 构造函数实例化 TVehicle,则此实例的所有字段都具有默认值,但我可以使用此 ad-hoc 实例来查找包含完整车辆的字典条目,我将在其中找到实际数据(来自数据库)及其关联的 TPerson。
  • @David 没有代码 - 它只是 Delphi 编程的基本 (OOP) 设计和实现问题 :)
【解决方案3】:

根据您的设计和其他方面的异味,我会回答您的问题,因为创建对象键控字典并根据与键的内存地址不同的任何内容进行比较是有效的:

您可以在创建 TDictionary 时创建一个新的比较器。

例如:

type
  TVehicleOwner = class (TDictionary<TVehicle, TOwner>)
  end;

//other code here

procedure TForm2.Button1Click(Sender: TObject);
var
  VehOwner: TVehOwner;
begin
  VehOwner := TVehOwner.Create(TEqualityComparer<TVehicle>.Construct(
    //comparer
    function(const Left, Right: TVehicle): Boolean
    begin
      { Make a case insensitive comparison }
      Result := CompareText(Left.FID, Right.FID) = 0;
    end,
    //hasher
    function(const Value: TVehicle): Integer
    begin
      { Generate a hash code. }
      Result := TheHashAlgorythmOfYourChoice(Value.FID);
    end)
  );

  //more code here

这就是说,如果您有两个代表同一个对象的实例,我认为这是您的代码中的一个缺陷。如果您在内存中有 ID 为“ABC”的 TVehicle,对我而言,这应该是该车辆的唯一实例,您必须提供某种方法来为所有代码获取相同的实例。这样,您可以在不编写自定义比较器的情况下使用 Dictionary 类,但更重要的是,您知道您一直在使用同一个对象,并且您的应用程序状态将与代码、UI 或其他界面中的任何内容保持一致.

【讨论】:

  • 在某些无法修改对象的区域中存在此类对象是很正常的 - 数据传输对象 (DTO),以前称为值对象(请参阅en.wikipedia.org/wiki/Data_transfer_object)。
  • @mjn,谢谢我知道 DTO ......你的方法似乎不是我们在谈论这个,无论如何 +1 对你的评论作为对未来读者的澄清
猜你喜欢
  • 2011-12-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-08-16
  • 1970-01-01
相关资源
最近更新 更多