Search code examples
c#.netgethashcode

Same structs have different HashCode


I have a code:

    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;
        }
    }

As you can see i used a struct and i remarked weird behavior. If i have input data like this:

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

Debug output is:

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

But if i change args. order to:

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

Debug out is:

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

And there is one thing that can be important is that in first case we have all "dividers"(GetFromPoints method) are positive (4,24,20) in second case one of them is negative and other two are positive (-4,20,24). Can anybody explain this?

UPD. when i changed

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

to

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

which means that 0 divided by a negative isn't 0?


Solution

  • Basically you are getting a negative zero value double. However the runtime's default GetHashCode for structs appears to blindly just combine the underlying bytes and not call the field's GetHashCode. Here is simplified version of what you are seeing:

    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;   
    }
    

    The output:

    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

    I've defined two double one of which is the "normal" zero and the other which is "negative" zero. The difference between the two is in the double's sign bit. The two values are equal in all apparent ways (Equals comparison, GetHashCode, ToString representation) except on the byte level. However when they are put into a custom struct the runtime's GetHashCode method just combines the raw bits which gives a different hash code for each struct even through they contain equal values. Equals does the same thing and gets a False result.

    I admit this is kind of big gotcha. The solution to this is to make sure to you override Equals and GetHashCode to get the proper equality that you want.

    Actually a similar issue has been mentioned before apparently the runtime only does this when the struct's fields are all 8 bytes wide.