我们遇到了一个神奇的十进制数字,打破了我们的哈希表。我把它归结为以下最小的情况:
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#怎么做错了?
答案 0 :(得分:27)
首先,C#完全没有做错任何事。这是一个框架错误。
它确实看起来像一个bug - 基本上,在比较相等性时涉及的规范化应该以相同的方式用于哈希码计算。我已经检查过并且可以重现它(使用.NET 4),包括检查Equals(decimal)
和Equals(object)
方法以及==
运算符。
它看起来肯定是问题的d0
值,因为向d1
添加尾随0不会改变结果(当然它与d0
相同) 。我怀疑那里有一些角落案例被那里的确切位代表所绊倒。
我很惊讶它不是(正如你所说,它在大多数当时工作),但你应该在Connect报告错误。
答案 1 :(得分: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
)看起来不错 - 我使用相同的代码来获取相同的字节数组小数。
答案 2 :(得分: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);
}
}
答案 3 :(得分:1)
这是一个十进制舍入错误。
使用.000000000000000设置d0需要太多的精度,因此负责它的算法会出错并最终给出不同的结果。在这个例子中它可以被归类为一个bug,虽然注意“十进制”类型应该具有 28位数的精度,并且在这里,你实际上要求d0的精度为29位。
可以通过询问d0和d1的完整原始十六进制表示来测试。
答案 4 :(得分:1)
我在VB.NET(v3.5)中对此进行了测试并得到了同样的结果。
关于哈希码的有趣之处:
A)0x40727800 = 1081243648
B)0xBF8D880F = -1081243648
使用Decimal.GetBits()我找到了
格式:尾数(hhhhhhhh hhhhhhhhhhhhhhhhh)指数(seee0000) (h是值,'s'是符号,'e'是指数,0必须是零)
d1 ==&gt; 00000000 00000000 00000B8B - 00010000 =(2955/10 ^ 1)= 295.5
做==&gt; 5F7B2FE5 D8EACD6E 2E000000 - 001A0000
...转换为29550000000000000000000000000/10 ^ 26 = 295.5000000 ......等
**编辑:好的,我写了一个128位十六进制小数计算器,上面的内容完全正确
它看起来像某种内部转换错误。 Microsoft明确声明他们不保证其GetHashCode的默认实现。如果您将它用于任何重要的事情,那么为小数类型编写自己的GetHashCode可能是有意义的。例如,将其格式化为固定的十进制,固定宽度字符串和散列似乎是有效的(&gt;小数点后29位,&gt; 58宽度 - 适合所有可能的小数)。
* 编辑:我不知道这个了。它仍然必须是转换错误,因为存储的精度从根本上改变了内存中的实际值。哈希码最终成为彼此的签名否定是一个很大的线索 - 需要进一步研究默认的哈希代码实现以找到更多。
28或29位数无关紧要,除非有依赖代码不能正确评估外部范围。可访问的最大96位整数是:
79228162514264337593543950335
所以你可以拥有29位数,只要整个事物(没有小数点)小于这个值。我不能不认为这在某处的哈希码计算中更为微妙。
答案 5 :(得分:-5)
documetation表示由于GetHashCode()
无法预测,您应该创建自己的。{3}}。它被认为是不可预测的,因为每个Type都有它自己的实现,因为我们不知道它的内部,我们应该根据我们评估唯一性的方式创建自己的。
但是,我认为答案是GetHashCode()
没有使用数学十进制值来创建哈希码。
在数学上我们看到295.50000000和295.5是相同的。当您查看IDE中的十进制对象时,这也是正确的。但是,如果您对两个小数都执行ToString()
,您将看到编译器看到它们的方式不同,即您仍将看到295.50000000。 GetHashCode()
显然没有使用小数的数学表示来创建哈希码。
你的修复只是创建一个没有所有尾随零的新小数,这就是它工作的原因。