相同的结构有不同的 HashCode

Same structs have different HashCode

我有一个代码:

    public class Point
    {
        public int x;
        public int y;
        public Point() { x = 0; y = 0; }
        public Point(int a, int b) { x = a; y = b; }
    }
    public struct Coefficients{
        public double a;
        public double b;
        public double c;
        public Coefficients(double a, double b, double c)
        {
            this.a = a;
            this.b = b;
            this.c = c;
        }
        public static Coefficients GetFromPoints(Point point1, Point point2)
        {

            int x1 = point1.x;
            int x2 = point2.x;
            int y1 = point1.y;
            int y2 = point2.y;
            double a = y1- y2;
            double b = x2 - x1;
            double c = x1 * y2 - y1 * x2 ;
            double max = Math.Max(Math.Max(a, b), c);
            double min= Math.Min(Math.Min(a, b), c);
            double divider = Math.Abs(max)> Math.Abs(min)?max:min;
            divider = Math.Abs(divider) > 1? divider : 1;
            return new Coefficients(a/divider, b/divider, c/divider);

        }

    }
public class Solution
    {
        public int MaxPoints(Point[] points)
        {
            var coef_list = new List<Coefficients>();
            for (var x = 0; x < points.Length - 1; x++)
            {
                for (var y = x + 1; y < points.Length; y++)
                {
                    var coef = Coefficients.GetFromPoints(points[x], points[y]);
                    coef_list.Add(coef);
                }
            }
            foreach (var item in coef_list) {
                Debug.WriteLine(item.a);
                Debug.WriteLine(item.b);
                Debug.WriteLine(item.c);
                Debug.WriteLine(item.GetHashCode());
                Debug.WriteLine("---------------");
            }           
            return 0;
        }
    }

如您所见,我使用了一个结构,并且发现了奇怪的行为。 如果我有这样的输入数据:

prg.MaxPoints(new Point[] { new Point(4, -1), new Point(4, 0), new Point(4, 5) });

调试输出为:

-0,25
0
1
-450335288
---------------
-0,25
0
1
-450335288
---------------
-0,25
0
1
-450335288
---------------

但是如果我改变参数。为了:

prg.MaxPoints(new Point[] { new Point(4, 0),new Point(4, -1) , new Point(4, 5) });

调试出来的是:

-0,25
0
1
1697148360
---------------
-0,25
0
1
-450335288
---------------
-0,25
0
1
-450335288
---------------

有一件事很重要,在第一种情况下,我们所有 "dividers"(GetFromPoints 方法)都是正的 (4,24,20)在第二种情况下,其中一个为负,另外两个为正 (-4,20,24)。 谁能解释一下?

UPD. 当我改变

return new Coefficients(a/divider, b/divider, c/divider);

return new Coefficients(a/divider, 0, c/divider);//anyway in all of these cases 2-nd argument is 0

这意味着 0 除以负数不是 0?

基本上你得到的是负零值加倍。然而,运行时的结构默认 GetHashCode 似乎只是盲目地组合底层字节而不调用字段的 GetHashCode。这是您所看到的简化版本:

public struct S
{
    public double value;

    public S(double d)
    {
        value = d;
    }
}

public static void Main(string[] args)
{           
    double d1 = 0;
    double d2 = d1 / -1;

    Console.WriteLine("using double");
    Console.WriteLine("{0} {1}", d1, d1.GetHashCode());
    Console.WriteLine(GetComponentParts(d1));
    Console.WriteLine("{0} {1}", d2, d2.GetHashCode());
    Console.WriteLine(GetComponentParts(d2));
    Console.WriteLine("Equals: {0}, Hashcode:{1}, {2}", d1.Equals(d2), d1.GetHashCode(), d2.GetHashCode());

    Console.WriteLine();
    Console.WriteLine("using a custom struct");

    var s1 = new S(d1);
    var s2 = new S(d2);
    Console.WriteLine(s1.Equals(s2));
    Console.WriteLine(new S(d1).GetHashCode());
    Console.WriteLine(new S(d2).GetHashCode());            
}

// from: https://msdn.microsoft.com/en-us/library/system.double.epsilon(v=vs.110).aspx
private static string GetComponentParts(double value)
{
    string result = String.Format("{0:R}: ", value);
    int indent = result.Length;

    // Convert the double to an 8-byte array.
    byte[] bytes = BitConverter.GetBytes(value);
    // Get the sign bit (byte 7, bit 7).
    result += String.Format("Sign: {0}\n", 
                          (bytes[7] & 0x80) == 0x80 ? "1 (-)" : "0 (+)");

    // Get the exponent (byte 6 bits 4-7 to byte 7, bits 0-6)
    int exponent = (bytes[7] & 0x07F) << 4;
    exponent = exponent | ((bytes[6] & 0xF0) >> 4);  
    int adjustment = exponent != 0 ? 1023 : 1022;
    result += String.Format("{0}Exponent: 0x{1:X4} ({1})\n", new String(' ', indent), exponent - adjustment);

    // Get the significand (bits 0-51)
    long significand = ((bytes[6] & 0x0F) << 48); 
    significand = significand | ((long) bytes[5] << 40);
    significand = significand | ((long) bytes[4] << 32);
    significand = significand | ((long) bytes[3] << 24);
    significand = significand | ((long) bytes[2] << 16);
    significand = significand | ((long) bytes[1] << 8);
    significand = significand | bytes[0];    
    result += String.Format("{0}Mantissa: 0x{1:X13}\n", new String(' ', indent), significand);    

    return result;   
}

输出:

using double
0 0
0: Sign: 0 (+)
Exponent: 0xFFFFFC02 (-1022)
Mantissa: 0x0000000000000

0 0
0: Sign: 1 (-)
Exponent: 0xFFFFFC02 (-1022)
Mantissa: 0x0000000000000

Equals: True, Hashcode:0, 0

using a custom struct
False
346948956
-1800534692

我定义了两个双精度值,一个是 "normal" 零,另一个是 "negative" 零。两者之间的区别在于 double 的符号位。除了字节级别之外,这两个值在所有明显的方式(等于比较、GetHashCode、ToString 表示)上都是相等的。但是,当将它们放入自定义结构时,运行时的 GetHashCode 方法只是组合原始位,即使它们包含相同的值,也会为每个结构提供不同的哈希码。 Equals 做同样的事情并得到错误的结果。

我承认这是个大问题。对此的解决方案是确保您重写 Equals 和 GetHashCode 以获得您想要的正确相等性。

实际上已经提到了一个类似的问题before显然运行时只在结构的字段都是 8 字节宽时才这样做。