C#为什么等号可能产生不相等的哈希值?

我们遇到了一个魔法十进制数,打破了我们的哈希表。 我把它归结为以下最小的情况:

decimal d0 = 295.50000000000000000000000000m; decimal d1 = 295.5m; Console.WriteLine("{0} == {1} : {2}", d0, d1, (d0 == d1)); Console.WriteLine("0x{0:X8} == 0x{1:X8} : {2}", d0.GetHashCode(), d1.GetHashCode() , (d0.GetHashCode() == d1.GetHashCode())); 

给出以下输出:

 295.50000000000000000000000000 == 295.5 : True 0xBF8D880F == 0x40727800 : False 

真正奇怪的是:更改,添加或删除d0中的任何数字,问题就消失了。 即使添加或删除一个尾随零! 标志似乎并不重要。

我们的解决办法是将值除以尾随的零,如下所示:

 decimal d0 = 295.50000000000000000000000000m / 1.000000000000000000000000000000000m; 

但是我的问题是,C#如何做错了?

首先,C#没有做任何错误的事情。 这是一个框架错误。

它确实看起来像一个错误 – 基本上任何规范化涉及到比较的平等应该以相同的方式用于哈希码计算。 我已经检查并可以重现(使用.NET 4),包括检查Equals(decimal)Equals(object)方法以及==运算符。

它看起来像d0值是问题所在,因为向d1添加尾随0不会改变结果(直到它与当然的d0相同)。 我怀疑有一些angular落案件在那里的确切位表示绊倒。

我很惊讶它不是(正如你所说,它大部分时间都在运行),但是你应该报告Connect上的错误。

另一个错误(?)导致在不同的编译器上以相同的小数表示不同的字节表示forms:尝试在VS 2005和VS 2010上编译以下代码。或者查看我在Code Project上的文章 。

 class Program { static void Main(string[] args) { decimal one = 1m; PrintBytes(one); PrintBytes(one + 0.0m); // compare this on different compilers! PrintBytes(1m + 0.0m); Console.ReadKey(); } public static void PrintBytes(decimal d) { MemoryStream memoryStream = new MemoryStream(); BinaryWriter binaryWriter = new BinaryWriter(memoryStream); binaryWriter.Write(d); byte[] decimalBytes = memoryStream.ToArray(); Console.WriteLine(BitConverter.ToString(decimalBytes) + " (" + d + ")"); } } 

有些人使用以下标准化代码d=d+0.0000m ,这在VS 2010上无法正常工作。您的规范化代码( d=d/1.000000000000000000000000000000000m )看起来不错 – 我使用同一个代码来得到相同的小数位。

也跑进这个臭虫… 🙁

testing(见下文)表明这取决于该值的最大精度。 错误的哈希码只出现在给定值的最大精度附近。 由于testing显示错误似乎取决于小数点左边的数字。 有时,只有maxDecimalDigits – 1的哈希码是错误的,有时候maxDecimalDigits的值是错误的。

 var data = new decimal[] { // 123456789012345678901234567890 1.0m, 1.00m, 1.000m, 1.0000m, 1.00000m, 1.000000m, 1.0000000m, 1.00000000m, 1.000000000m, 1.0000000000m, 1.00000000000m, 1.000000000000m, 1.0000000000000m, 1.00000000000000m, 1.000000000000000m, 1.0000000000000000m, 1.00000000000000000m, 1.000000000000000000m, 1.0000000000000000000m, 1.00000000000000000000m, 1.000000000000000000000m, 1.0000000000000000000000m, 1.00000000000000000000000m, 1.000000000000000000000000m, 1.0000000000000000000000000m, 1.00000000000000000000000000m, 1.000000000000000000000000000m, 1.0000000000000000000000000000m, 1.00000000000000000000000000000m, 1.000000000000000000000000000000m, 1.0000000000000000000000000000000m, 1.00000000000000000000000000000000m, 1.000000000000000000000000000000000m, 1.0000000000000000000000000000000000m, }; for (int i = 0; i < 1000; ++i) { var d0 = i * data[0]; var d0Hash = d0.GetHashCode(); foreach (var d in data) { var value = i * d; var hash = value.GetHashCode(); Console.WriteLine("{0};{1};{2};{3};{4};{5}", d0, value, (d0 == value), d0Hash, hash, d0Hash == hash); } } 

这是一个小数舍入错误。

如果将d0设置为.000000000000000,则需要太高的精确度,因此负责该algorithm的algorithm出错,并最终导致不同的结果。 在这个例子中,它可以被分类为一个bug,不过请注意,“decimal”types的精度应该是28位 ,这里实际上需要d0的精度为29位。

这可以通过询问d0和d1的完整原始hex表示来testing。

我在VB.NET(v3.5)中testing了这个,得到了同样的结果。

有关哈希代码的有趣的事情:

A)0x40727800 = 1081243648

B)0xBF8D880F = -1081243648

使用Decimal.GetBits()我find了

格式:尾数(hhhhhhhhhhhhhhhhhhhhhhhh)指数(seee0000)(h是值,'s'是符号,'e'是指数,0必须是零)

d1 ==> 00000000 00000000 00000B8B – 00010000 =(2955/10 ^ 1)= 295.5

做==> 5F7B2FE5 D8EACD6E 2E000000 – 001A0000

…转换为295500000000000000000000000/10 ^ 26 = 295.5000000 …等

**编辑:好的,我写了一个128位hex计算器,上面的是完全正确的

它绝对看起来像某种内部转换错误。 Microsoft明确指出,他们不保证它们的GetHashCode的默认实现。 如果你正在使用它来做任何重要的事情,那么编写你自己的GetHashCode(十进制types)可能是有意义的。 格式化到一个固定的十进制,固定宽度的string和哈希似乎工作,例如(> 29位小数,> 58宽度 – 适合所有可能的小数)。

*编辑:我不知道这个了。 由于存储的精度从根本上改变了存储器中的实际值,所以它仍然必须是某处的转换错误。 哈希码作为彼此签名的负面结果是一个很大的线索 – 需要进一步查看默认的哈希码实现来find更多。

28或29位数字应该没有关系,除非有相关代码不适当地评估外部范围。 可访问的最大的96位整数是:

79228162514264337593543950335

所以只要整个事物(没有小数点)小于这个值就可以有29个数字。 我不禁认为这是在哈希码计算某处更微妙的东西。

该文档build议,因为GetHashCode()是不可预知的,你应该创build自己的。 它被认为是不可预测的,因为每个types都有它自己的实现,因为我们不知道它的内部,我们应该根据我们如何评估唯一性创build自己的内部。

不过,我认为答案是GetHashCode()不使用math十进制值来创build哈希码。

在math上我们看到295.50000000和295.5是相同的。 在IDE中查看小数对象时也是如此。 但是,如果你在两个小数点上都做一个ToString() ,你会看到编译器看到它们的方式不同,即你仍然会看到295.50000000。 GetHashCode()显然不使用小数的math表示来创build哈希码。

您的修复只是创build一个新的小数点,而不是所有的尾随零,这就是为什么它的工作原理。