我们遇到了一个魔术的十进制数字,破坏了我们的哈希表。我将其简化为以下最小情况:
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#完全没有做错任何事情。这是一个[[framework错误。
尽管确实确实看起来像个错误-基本上,在比较相等性时涉及的任何规范化都应以相同的方式用于哈希码计算。我已经检查过并且也可以重现它(使用.NET 4),包括检查Equals(decimal)
和Equals(object)
方法以及==
运算符。肯定看起来像是d0
值是问题所在,因为在d1
上添加尾随0不会改变结果(直到与d0
相同为止)。我怀疑那里有一些特殊情况下的角位被绊倒了。
令我感到惊讶的是,它不是(而且您说,它在
大多数
时间内起作用),但是您应该报告Connect上的错误。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
)看起来不错-我使用相同的代码来获得相同的字节数组,并且使用相同的小数位数。
测试(请参见下文)表明,这取决于该值的最大精度。错误的哈希码只会在给定值的最大精度附近发生。如测试所示,错误似乎取决于小数点左边的数字。有时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所需的精度太高,结果,负责它的算法会出错并最终给出不同的结果。在此示例中,它可能被归类为错误,尽管请注意,“十进制”类型应该具有
28位精度],在这里,您实际上要求d0的精度为29位。可以通过要求d0和d1的完整原始十六进制表示来测试。
GetHashCode()
不可预测,因此您应该创建自己的。之所以认为这是不可预测的,是因为每种类型都有其自己的实现,并且由于我们不了解其内部结构,因此应根据评估唯一性的方式来创建自己的类型。