C# 为什么相等的小数会产生不相等的哈希值
本文关键字:不相等 哈希值 小数 为什么 | 更新日期: 2023-09-27 18:18:08
我们遇到了一个神奇的十进制数,打破了我们的哈希表。我将其归结为以下最小情况:
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#怎么做错了?
编辑:刚刚注意到这已在.NET Core 3.0中修复(可能更早,我没有检查(:https://dotnetfiddle.net/4jqYos
首先,C#根本没有做错任何事。这是一个框架错误。
不过,它确实看起来像一个错误 - 基本上,比较相等所涉及的任何规范化都应该以相同的方式用于哈希代码计算。我已经检查过并且可以重现它(使用 .NET 4(,包括检查Equals(decimal)
和Equals(object)
方法以及==
运算符。
看起来d0
值肯定是问题所在,因为将尾随 0 添加到 d1
不会改变结果(当然,直到它与 d0
相同(。我怀疑那里有一些极端情况被确切的位表示所绊倒。
我很惊讶它不是(正如你所说,它大部分时间都有效(,但你应该在 Connect 上报告这个错误。
另一个错误(?(导致不同编译器上同一小数点的不同字节表示形式:尝试在VS 2005和VS 2010上编译以下代码。或者看看我关于代码项目的文章。
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);
}
}
这是一个小数舍入错误。
用 .00000000000000000设置 d0 需要太多的精度,结果负责它的算法犯了一个错误,最终给出了不同的结果。在此示例中,它可以归类为错误,但请注意,"十进制"类型的精度应为 28 位,而在这里,您实际上要求 d0 的精度为 29 位。
这可以通过请求 d0 和 d1 的完整原始十六进制表示形式来测试。
我在 VB.NET (v3.5( 中对此进行了测试并得到了同样的东西。
关于哈希码的有趣之处:
A( 0x40727800 = 1081243648
B( 0xBF8D880F = -1081243648
使用Decimal.GetBits((我发现
格式 : 尾数 (hh(h 是值,'s' 是符号,'e' 是指数,0 必须是零(
d1 ==> 00000000 00000000 00000B8B - 00010000 = (2955/10 ^ 1( = 295.5
do ==> 5F7B2FE5 D8EACD6E 2E000000 - 001A0000
。转换为 295500000000000000000000000000000/10^26 = 295.5000000...etc
**编辑:好的,我写了一个128位十六进制计算器,上面完全正确
它绝对看起来像某种内部转换错误。Microsoft明确声明,他们不保证GetHashCode的默认实现。 如果您将其用于任何重要的事情,那么为十进制类型编写自己的 GetHashCode 可能是有意义的。 例如,将其格式化为固定的小数、固定宽度字符串和哈希似乎有效(小数点后>29 位,> 58 宽度 - 适合所有可能的小数(。
*编辑:我不再知道这个了。 它仍然必须是某处的转换错误,因为存储的精度从根本上改变了内存中的实际值。 哈希代码最终成为彼此的签名否定是一个很大的线索 - 需要进一步研究默认哈希代码实现以找到更多信息。
28 或 29 位数字无关紧要,除非存在无法正确评估外部扩展数据的依赖代码。 可访问的最大 96 位整数是:
79228162514264337593543950335
因此,只要整个事物(不带小数点(小于此值,您就可以有 29 位数字。 我不禁认为,这在某处的哈希代码计算中要微妙得多。
文档表明,由于GetHashCode()
不可预测,您应该创建自己的文档。它被认为是不可预测的,因为每个类型都有自己的实现,并且由于我们不知道它的内部结构,我们应该根据我们如何评估唯一性来创建自己的实现。
但是,我认为答案是GetHashCode()
没有使用数学十进制值来创建哈希代码。
在数学上,我们认为 295.50000000 和 295.5 是相同的。当您查看 IDE 中的十进制对象时,也是如此。但是,如果你对两个小数点都做ToString()
,你会看到编译器对它们的看法不同,即你仍然会看到 295.50000000。 GetHashCode()
显然没有使用小数的数学表示来创建哈希代码。
您的解决方法只是创建一个没有所有尾随零的新十进制,这就是它工作的原因。