Laden...

Fehler in GetHashCode im Framework?

Erstellt von Uwe81 vor 12 Jahren Letzter Beitrag vor 12 Jahren 807 Views
U
Uwe81 Themenstarter:in
282 Beiträge seit 2008
vor 12 Jahren
Fehler in GetHashCode im Framework?

Die GetHashCode-Methode von Structs ist IMO standardmäßig Fehlerhaft:

Aus ValueType.cs


        /*=================================GetHashCode==================================
        **Action: Our algorithm for returning the hashcode is a little bit complex.  We look
        **        for the first non-static field and get it's hashcode.  If the type has no
        **        non-static fields, we return the hashcode of the type.  We can't take the 
        **        hashcode of a static member because if that member is of the same type as
        **        the original type, we'll end up in an infinite loop. 
        **Returns: The hashcode for the type. 
        **Arguments: None.
        **Exceptions: None. 
        ==============================================================================*/

Der Kommentar ist aber falsch. Eine Korrektur steht in Why is ValueType.GetHashCode() implemented like it is?

The actual implementation of ValueType.GetHashCode() doesn't quite match the comment. It has two versions of the algorithm, fast and slow. It first checks if the struct contains any members of a reference type and if there is any padding between the fields. Padding is empty space in a structure value, created when the JIT compiler aligns the fields. There's padding in a struct that contains bool and int (3 bytes) but no padding when it contains int and int, they fit snugly together.

Without a reference and without padding, it can do the fast version since every bit in the structure value is a bit that belongs to a field value. It simply xors 4 bytes at a time. You'll get a 'good' hash code that considers all the members. Many simple structure types in the .NET framework behave this way, like Point and Size.

Failing that test, it does the slow version, the moral equivalent of reflection. That's what you get, your KeyValuePair<> contains references. And this one only checks the first candidate field, like the comment says. This is surely a perf optimization, avoiding burning too much time.

Dieser Kommentar wirkt plausibel, wie folgendes Beispiel zeigt:


using System;

struct MyObjStruct {
    public object x;
    public object y;
}

struct MyIntStruct {
    public long x;
    public long y;
}


public class Program {
    static object x = new object();
    static object y1 = new object();
    static object y2 = new object();

    public static void Main() {
        var os1 = new MyIntStruct { x = 1, y = 3 };
        var os2 = new MyIntStruct { x = 1, y = 4 };
        Console.WriteLine(os1.GetHashCode() == os2.GetHashCode()); //False

        var is1 = new MyObjStruct { x = x, y = y1 };
        var is2 = new MyObjStruct { x = x, y = y2 };
        Console.WriteLine(is1.GetHashCode() == is2.GetHashCode()); //True
    }
}

Nun schauen wir uns mal die Equals-Methode aus ValueType.cs an


        public override bool Equals (Object obj) { 
            BCLDebug.Perf(false, "ValueType::Equals is not fast.  "+this.GetType().FullName+" should override Equals(Object)"); 
            if (null==obj) {
                return false; 
            }
            RuntimeType thisType = (RuntimeType)this.GetType();
            RuntimeType thatType = (RuntimeType)obj.GetType();
 
            if (thatType!=thisType) {
                return false; 
            } 

            Object thisObj = (Object)this; 
            Object thisResult, thatResult;

            // if there are no GC references in this object we can avoid reflection
            // and do a fast memcmp 
            if (CanCompareBits(this))
                return FastEqualsCheck(thisObj, obj); 
 
            FieldInfo[] thisFields = thisType.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
 
            for (int i=0; i<thisFields.Length; i++) {
                thisResult = ((RtFieldInfo)thisFields[i]).InternalGetValue(thisObj,false);
                thatResult = ((RtFieldInfo)thisFields[i]).InternalGetValue(obj, false);
 
                if (thisResult == null) {
                    if (thatResult != null) 
                        return false; 
                }
                else 
                if (!thisResult.Equals(thatResult)) {
                    return false;
                }
            } 

            return true; 
        } 

Fazit: Sind alle Member ValueTypen und können ohne Padding aligned werden, dann wird sowohl für Equals als auch für GetHashCode das Bitmuster verwendet. Ansonsten werden die GetHashCode() des ersten Member und die Equals() aller Member aufgerufen.

Testen wir das:


using System;


struct MyDifference {
    public int x;
    public int y;

    public override bool Equals(object obj) {
        if (!(obj is MyDifference)) return false;
        var other = (MyDifference) obj;
        return x - y == other.x - other.y;
    }

    public override int GetHashCode() {
        return x - y;
    }
}



struct MyObjStruct {
    public MyDifference x;
    public object y;
}



struct MyLongStruct {
    public MyDifference x;
    public long y;
}



public class Program {
    public static void Main() {
        var y = new object();

        var md1 = new MyDifference {x = 5, y = 2};
        var md2 = new MyDifference {x = 6, y = 3};
        Console.WriteLine("Equals myDiff: " + md1.Equals(md2));
        Console.WriteLine("HashCd myDiff: " + (md1.GetHashCode() == md2.GetHashCode()));

        var os1 = new MyObjStruct {x = md1, y = y};
        var os2 = new MyObjStruct {x = md2, y = y};
        Console.WriteLine("Equals objStruct: " + os1.Equals(os2));
        Console.WriteLine("HashCd objStruct: " + (os1.GetHashCode() == os2.GetHashCode()));

        var ls1 = new MyLongStruct {x = md1, y = 9};
        var ls2 = new MyLongStruct {x = md2, y = 9};
        Console.WriteLine("Equals longStruct: " + ls1.Equals(ls2));
        Console.WriteLine("HashCd longStruct: " + (ls1.GetHashCode() == ls2.GetHashCode()));
    }
}

Zu erwarten:
Für die MyDifference müsste beides True ergeben.

Für die MyObjStruct müsste per Reflection sowohl Equals als auch GetHashCode (für das erste Feld) aufgerufen werden, also beide male True ausgegeben werden.

Für die MyLongStruct müssten die Bitmuster verwendet werden (also weder GetHachCode noch Equals aufgerufen werden), und beide male False ausgegeben werden.

Die Ausgabe ist aber


Equals myDiff: True
HashCd myDiff: True
Equals objStruct: True
HashCd objStruct: False
Equals longStruct: True
HashCd longStruct: False

Also ist, obwohl MyDiff eine valide GetHashCode und Equals anbietet, die Implementierung von GetHashCode sowohl für MyObjStruct als auch für MyLongStruct falsch, weil Equals True ergibt und GetHashCode false ist.

Im Debugger sieht mann, dass sowohl by MyObjStruct.Equals() als auch bei MyLongStruct.Equals() die MyDifference.Equals() aufgerufen wird, hingegen in keinem der GetHashCodes die GetHashCode der MyDifference.

Ich habe das auf Win7 64 bit, Vs2010 kompiliert als .NET 4.0;

Das Ergbnis ändert sich nicht, wenn man im MyLongStruct das y durch ein int ersetzt. Es ist sowohl für int y als auch für long y unabhängig vom Kompilieren als x64 oder x86.

Auch das steht eigentlich in StackOverflow

One more excruciating detail: the fast version has a bug that bytes when the structure contains a field of a type decimal. The values 12m and 12.0m are logically equal but they don't have the same bit pattern. GetHashCode() will say that they are not equal.

1.130 Beiträge seit 2007
vor 12 Jahren

Ich kann deine erkenntnisse nur bestätigen. Allerdings weist m$ dieser sorte von bugs gerne ein won'tfix zu, da es sonst zu kompatibilitätsproblemen kommt. Workaround ist halt, dass man überall GetHashCode so implementiert, wie mans grade braucht.

Projekte:Jade, HttpSaver
Zum Rechtschreiben gibts doch schon die Politiker. Aber die bauen auch nur mist!