Warning: file_get_contents(/data/phpspider/zhask/data//catemap/2/csharp/282.json): failed to open stream: No such file or directory in /data/phpspider/zhask/libs/function.php on line 167

Warning: Invalid argument supplied for foreach() in /data/phpspider/zhask/libs/tag.function.php on line 1116

Notice: Undefined index: in /data/phpspider/zhask/libs/function.php on line 180

Warning: array_chunk() expects parameter 1 to be array, null given in /data/phpspider/zhask/libs/function.php on line 181
C#为什么相等的小数可以产生不相等的散列值?_C#_.net_Hash_Decimal - Fatal编程技术网

C#为什么相等的小数可以产生不相等的散列值?

C#为什么相等的小数可以产生不相等的散列值?,c#,.net,hash,decimal,C#,.net,Hash,Decimal,我们遇到了一个神奇的十进制数,它打破了我们的哈希表。我把它归结为以下最基本的情况: 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(

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

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#怎么会做错这件事

编辑:刚刚注意到这一点在.NETCore3.0中得到了修复(可能更早,我没有检查):

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

不过,它看起来确实像一个bug——基本上,在比较等式时涉及的任何规范化都应该以同样的方式用于哈希代码计算。我已经检查并可以复制它(使用.NET4),包括检查
Equals(decimal)
Equals(object)
方法以及
=
操作符

显然,问题出在
d0
值上,因为向
d1
添加尾随0不会改变结果(当然,在与
d0
相同之前)。我怀疑某个角落的案子被确切的比特表示绊倒了

我很惊讶它不是(正如你所说,它大部分时间都能工作),但是你应该报告错误

建议,由于
GetHashCode()
不可预测,您应该创建自己的。它被认为是不可预测的,因为每种类型都有它自己的实现,而且由于我们不知道它的内部结构,我们应该根据我们如何评估唯一性来创建自己的实现

但是,我认为答案是
GetHashCode()
没有使用数学十进制值来创建哈希代码

从数学上讲,我们认为295.50000000和295.5是相同的。当您查看IDE中的十进制对象时,这也是正确的。但是,如果对这两个小数点执行
ToString()
,编译器会以不同的方式看待它们,即仍然会看到295.50000000
GetHashCode()
显然没有使用十进制的数学表示来创建哈希代码


您的解决方案只是创建一个没有所有尾随零的新小数,这就是它工作的原因。

这是一个小数舍入错误

将d0设置为.000000000000000需要太多的精度,因此负责它的算法会出错,并最终给出不同的结果。在本例中,它可能被归类为一个bug,不过请注意,“decimal”类型应该具有28位的精度,而在这里,d0实际上需要29位的精度

这可以通过询问d0和d1的完整原始十六进制表示来测试。

我在VB.NET(v3.5)中测试了这一点,得到了同样的结果

散列码的有趣之处在于:

A) 0x40727800=1081243648

B) 0xBF8D880F=-1081243648

使用Decimal.GetBits()我找到了

格式:尾数指数(见E0000) (h是值,“s”是符号,“e”是指数,0必须是零)

d1==>00000000000000000000000B8B-00010000 =(2955/10^1)=295.5

do==>5F7B2FE5 D8EACD6E 2000000-001A0000

…转换为29550000000000000000000/10^26=2955000000…等

**编辑:好的,我写了一个128位十六进制十进制计算器,上面的内容完全正确

它看起来确实像某种内部转换错误。Microsoft明确声明,他们不保证默认的GetHashCode实现。如果您将它用于任何重要的事情,那么为decimal类型编写自己的GetHashCode可能是有意义的。例如,将其格式化为固定小数、固定宽度字符串和哈希似乎是可行的(>29位小数,>58位宽度-适合所有可能的小数)

*编辑:我再也不知道这件事了。由于存储精度从根本上改变了内存中的实际值,所以在某些地方它一定是一个转换错误。散列码最终成为彼此的有符号负数是一条重要线索——需要进一步研究默认的散列码实现才能找到更多

28或29位数字应该无关紧要,除非存在不正确计算外部范围的依赖代码。可访问的最大96位整数为:

79228162514264337593543950335

因此,只要整件事(不含小数点)小于此值,就可以有29位数字。我忍不住想,在散列码计算中,这是一个更加微妙的问题。

另一个错误(?)导致同一个十进制数在不同的编译器上有不同的字节表示:尝试在VS2005和VS2010上编译以下代码。或者看看我的代码项目

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.000000000000000000000000000m
)看起来不错-我使用相同的代码为相同的小数获取相同的字节数组。

也遇到了此错误…:-(

测试(见下文)指示这取决于值的最大可用精度。错误的哈希代码仅出现在给定值的最大精度附近。测试表明,错误似乎取决于小数点左侧的数字。有时,maxDecimalDigits-1的唯一哈希代码错误,有时MaxDecimalDigital的值错误这是错误的

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);
    }
}
var数据=新的十进制[]{
//    123456789012345678901234567890
1.0米,
1.00米,
1000米,
1.000米,
10000米,
一百万,
一百万,
一百万,
1.000000000米,
一百万,
1.00000000000m,
1