C#为什么相等的小数会产生不相等的哈希值?

问题描述 投票:42回答:6

我们遇到了一个魔术的十进制数字,破坏了我们的哈希表。我将其简化为以下最小情况:

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#如何做到这一点?

edit:刚注意到这已在.NET Core 3.0中得到修复(可能是更早的版本,我没有检查):https://dotnetfiddle.net/4jqYos

c# .net hash decimal
6个回答
27
投票

首先,C#完全没有做错任何事情。这是一个[[framework错误。

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

肯定看起来像是d0值是问题所在,因为在d1上添加尾随0不会改变结果(直到与d0相同为止)。我怀疑那里有一些特殊情况下的角位被绊倒了。

令我感到惊讶的是,它不是(而且您说,它在

大多数

时间内起作用),但是您应该报告Connect上的错误。

4
投票
[另一个错误(?),导致在不同的编译器上使用相同的小数表示不同的字节:尝试在VS 2005和VS 2010上依次编译以下代码。或者在代码项目上查看我的article

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)看起来不错-我使用相同的代码来获得相同的字节数组,并且使用相同的小数位数。

3
投票
也遇到了这个错误...:-(

测试(请参见下文)表明,这取决于该值的最大精度。错误的哈希码只会在给定值的最大精度附近发生。如测试所示,错误似乎取决于小数点左边的数字。有时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); } }


1
投票
这是一个十进制舍入错误。

将d0设置为.000000000000000所需的精度太高,结果,负责它的算法会出错并最终给出不同的结果。在此示例中,它可能被归类为错误,尽管请注意,“十进制”类型应该具有

28位精度],在这里,您实际上要求d0的精度为29位。可以通过要求d0和d1的完整原始十六进制表示来测试。


1
投票
我在VB.NET(v3.5)中进行了测试,得到了同样的东西。

-5
投票
documetation建议由于GetHashCode()不可预测,因此您应该创建自己的。之所以认为这是不可预测的,是因为每种类型都有其自己的实现,并且由于我们不了解其内部结构,因此应根据评估唯一性的方式来创建自己的类型。
© www.soinside.com 2019 - 2024. All rights reserved.