【问题标题】:Converting a Double to an Integer for GetHashCode in Delphi在 Delphi 中为 GetHashCode 将 Double 转换为 Integer
【发布时间】:2010-11-20 06:21:45
【问题描述】:

Delphi 2009 将 GetHashCode 函数添加到 TObject。 GetHashCode 返回一个 Integer,用于 TDictionary 中的散列。

如果您希望一个对象在 TDictionary 中正常工作,您需要适当地覆盖 GetHashCode,这样通常不同的对象返回不同的整数哈希码。

但是对于包含双字段的对象,你会怎么做呢?如何将这些双精度值转换为 GetHashCode 的整数?

在 Java 中通常使用的方法是使用 Double.doubleToLongBits 或 Float.floatToIntBits 之类的方法。后者的文档描述如下:“根据 IEEE 754 浮点“单一格式”位布局返回指定浮点值的表示。”这涉及到对浮点值的不同位使用不同掩码的一些按位运算。

Delphi 中是否有这样的功能?

【问题讨论】:

  • 为什么需要改变它?默认的 GetHashCode 返回对象的内存地址,根据定义,每个对象都是唯一的。
  • 我认为如果要覆盖 Equals,如果您希望对象作为字典中的键工作,则需要覆盖 GetHashCode。有时你想重写 Equals 以便比较对象的字段来测试两个对象是否相等,而不是仅仅测试它们是否是完全相同的实例。

标签: delphi hash double gethashcode


【解决方案1】:

我建议对 Gamecat 代码进行以下改进:

type
  TVarRec = record
    case Integer of
      0: ( FInt1, FInt2 : Integer; )
      1: ( FDouble : Double; )
  end;

function Convert(const ADouble: Double): Integer;
var
  arec : TVarRec;
begin
  arec.FDouble := ADouble;
  Result := arec.FInt1 xor arec.FInt2;
end;

这会考虑到 Double 值的所有位。

(cmets 不适用于代码)

【讨论】:

  • 感谢您的改进 ;-)。
【解决方案2】:

如果要将双精度数映射到整数,可以使用变体记录:

type
  TVarRec = record
    case Integer of
      0: ( FInt : Integer; )
      1: ( FDouble : Double; )
  end;


function Convert(const ADouble: Double): Integer;
var
  arec : TVarRec;
begin
  arec.FDouble := ADouble;
  Result := arec.FInt;
end;

请注意,这会在不解释值的情况下进行按位复制。

另一个(一种肮脏的技巧,是使用绝对变量:

function Convert(const ADouble: Double): Integer;
var
  tempDouble : Double;
  tempInt    : Integer absolute tempDouble; // tempInt is at the same memory position as tempDouble.
begin
  tempDouble := ADouble;
  Result := tempInt;
end;

【讨论】:

  • 感谢 Gamecat。这些方法似乎适用于某些双精度数,但是您会得到很多给出相同整数数字的双精度数。例如,似乎所有整数都给出了一个零整数值。是否有可能以某种方式对此进行改进以减少哈希码相同的机会?或者这仅仅是因为我正在使用常规的数字模式进行测试?
  • 这行不通,因为 sizeof(double) = 8,而 sizeof(integer) = 4。不过,如果需要,您可以将两个整数映射到 double 上...
【解决方案3】:

真的没有必要做这样的事情,因为 GetHashCode 的默认值已经返回了一个保证每个对象都是唯一的数字:对象的内存地址。此外,如果您更改对象包含的数据,默认哈希值不会改变。

假设您有一个对象,其中包含一个值为 3.5 的 Double,您对其进行哈希处理并将其放入字典中,得到哈希码 12345678。您还有其他东西持有对它的引用,并且 Double 字段被更改,现在它的值为 5.21。下次您尝试计算其哈希值时,您的哈希码现在是 23456789,您的查找将失败。

除非您可以保证这永远不会发生,并且您有充分的理由不使用内存地址,否则最好的选择是保持 GetHashCode 不变。 (如果它没有坏,就不要修理它。)

【讨论】:

  • 确实,我认为如果您将可变对象用作哈希表中的键,然后在它们位于哈希表中时对其进行修改,则会造成麻烦。但我认为如果你覆盖 Equals,你需要覆盖 GetHashCode。无论如何,这就是它在 Java 中的工作方式。在这方面,Delphi 有什么不同(我对 Delphi 并没有那么了解)?据我了解,哈希表(可能是 TDictionary)通常同时依赖 GetHashCode 和 Equals 来定位特定项目。
  • 它依赖于它们两个,但它独立地依赖于它们。当你改变另一个时,没有必要改变一个。有关如何使用值的详细信息,请参阅 TDictionary.ContainsValue 和 TDictionary.GetBucketIndex。
  • 嗯,是的,我想你就在那里。 GetBucketIndex 使用 TDictionary 内部的 FComparer,但默认情况下,它看起来像进行身份比较。因此,虽然在 Java 中“相等的对象必须具有相等的哈希码”是规则,这意味着您必须大量覆盖 hashCode,但在 Delphi 中似乎并非如此......这很好:)
  • 是的。您可以在 System.pas 中查找默认函数,您会发现它们之间没有相互依赖关系。默认情况下,相等的对象在 Delphi 中具有相等的哈希码,但这不是必需的。
  • 其实...我刚刚使用 TDictionary 编写了几个简单的测试用例。我制作了一个包含一个字段的对象 - 一个整数。如果其中两个对象包含相同的整数,我希望它们被认为是相等的,所以我重写了 Equals 来指定它。但我也想将这些对象用作 TDictionary 中的键。我希望能够通过拥有一个相等的对象(根据我的 Equals,而不是内存地址相等)来访问任何特定键的值。无论如何,除非我以某种方式搞砸了,否则我的测试表明,如果你想让它工作,你实际上需要覆盖 GetHashCode 和 Equals。
【解决方案4】:

我猜Java的东西可以像这样在Delphi中实现:

type
  TVarRec = record
    case Integer of
      0: ( FInt1: Integer; )
      1: ( FSingle: Single; )
  end;

function GetHashCode(Value: Double): Integer;
var
  arec: TVarRec;
begin
  arec.FSingle := Value;
  Result := arec.FInt1;
end;

背后的想法是降低 Double 值的精度以匹配 Integer 的二进制大小(Sizeof(Single) = Sizeof(Integer))。如果您的值可以用单精度表示而不会发生冲突,那么这将提供一个很好的哈希值。

编辑:由于 typecast 在我的 D2009 中无法编译,我调整了变体记录解决方案。

【讨论】:

  • 这个主意不错,但如果 Value 大于 Single 允许的最大大小,会不会造成麻烦?此外,如果你有很多双精度数都舍入到相同的整数值,你会得到很多重复的哈希码。
  • 嗯,我不知道你的价值观,但 MaxSingle=3.4e+38,差不多。碰撞的概率不是那么高,因为这些值不是四舍五入为整数,而是转换为整数。对 Integer 的 Single 强制转换具有相同的位表示,但 Integer 值根本没有意义。
【解决方案5】:

对 Double 数据使用 CRC32,因为 xor 是邪恶的。

program Project1;

{$APPTYPE CONSOLE}

uses
  SysUtils;

type
  TVarRec = record
    case Integer of
      0: ( FInt1, FInt2 : Integer; );
      1: ( FDouble : Double; );
  end;

function Convert(const ADouble: Double): Integer;
var
  arec : TVarRec;
begin
  arec.FDouble := ADouble;
  Result := arec.FInt1 xor arec.FInt2;
end;

var
  FDoubleVar1, FDoubleVar2: TVarRec;
  HashCode1, HashCode2: Integer;
begin
  // Make a Double
  FDoubleVar1.FInt1 := $DEADC0DE;
  FDoubleVar1.FInt2 := $0C0DEF00;

  // Make another Double
  FDoubleVar2.FInt1 := $0C0DEF00;
  FDoubleVar2.FInt2 := $DEADC0DE;

  WriteLn('1rst Double   : ', FDoubleVar1.FDouble);
  WriteLn('2nd Double    : ', FDoubleVar2.FDouble);

  HashCode1 := Convert(FDoubleVar1.FDouble);
  HashCode2 := Convert(FDoubleVar2.FDouble);

  WriteLn('1rst HashCode : ', HashCode1);
  WriteLn('2nd HashCode  : ', HashCode2);

  if HashCode1 = HashCode2 then
  begin
    WriteLn('Warning: Same HashCode!');
  end;
  ReadLn;
end.

【讨论】:

  • 我在上面添加了一个示例,由于异或,不同的 Doubles 给出了相同的哈希码。
  • 哈希表总是会导致冲突,尤其是对于双精度 - 一个比所有可能的哈希值集合大得多的集合。
猜你喜欢
  • 2014-12-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-09-30
相关资源
最近更新 更多