GetHashCode-Richtlinien in C #

136

Ich habe im Essential C # 3.0- und .NET 3.5-Buch Folgendes gelesen:

Die Rückgabe von GetHashCode () über die Lebensdauer eines bestimmten Objekts sollte konstant sein (der gleiche Wert), auch wenn sich die Daten des Objekts ändern. In vielen Fällen sollten Sie die Methodenrückgabe zwischenspeichern, um dies zu erzwingen.

Ist das eine gültige Richtlinie?

Ich habe einige in .NET integrierte Typen ausprobiert, die sich nicht so verhalten haben.

Joan Venge
quelle
Wenn möglich, sollten Sie die akzeptierte Antwort ändern.
Giffyguy

Antworten:

93

Die Antwort ist meistens, es ist eine gültige Richtlinie, aber vielleicht keine gültige Regel. Es erzählt auch nicht die ganze Geschichte.

Der Punkt ist, dass Sie für veränderbare Typen den Hash-Code nicht auf den veränderlichen Daten basieren können, da zwei gleiche Objekte denselben Hash-Code zurückgeben müssen und der Hash-Code für die Lebensdauer des Objekts gültig sein muss. Wenn sich der Hash-Code ändert, erhalten Sie ein Objekt, das in einer Hash-Sammlung verloren geht, weil es nicht mehr im richtigen Hash-Bin gespeichert ist.

Zum Beispiel gibt Objekt A einen Hash von 1 zurück. Es geht also in Bin 1 der Hash-Tabelle. Dann ändern Sie Objekt A so, dass es einen Hash von 2 zurückgibt. Wenn eine Hash-Tabelle danach sucht, sucht sie in Bin 2 und kann es nicht finden - das Objekt ist in Bin 1 verwaist. Deshalb muss der Hash-Code nicht für die Lebensdauer des Objekts ändern , und nur ein Grund, warum das Schreiben von GetHashCode-Implementierungen ein Problem ist.

Update
Eric Lippert hat einen Blog gepostet , der hervorragende Informationen zu bietet GetHashCode.

Zusätzliches Update
Ich habe oben einige Änderungen vorgenommen:

  1. Ich habe zwischen Richtlinie und Regel unterschieden.
  2. Ich habe "für die Lebensdauer des Objekts" durchgestrichen.

Eine Richtlinie ist nur eine Richtlinie, keine Regel. In der Realität müssen GetHashCodediese Richtlinien nur befolgt werden, wenn erwartet wird, dass das Objekt den Richtlinien entspricht, z. B. wenn es in einer Hash-Tabelle gespeichert wird. Wenn Sie niemals beabsichtigen, Ihre Objekte in Hash-Tabellen zu verwenden (oder etwas anderes, das auf den Regeln von basiert GetHashCode), muss Ihre Implementierung nicht den Richtlinien folgen.

Wenn Sie "für die Lebensdauer des Objekts" sehen, sollten Sie "für die Zeit, die das Objekt benötigt, um mit Hash-Tabellen zusammenzuarbeiten" oder ähnliches lesen. Wie bei den meisten Dingen geht GetHashCodees darum zu wissen, wann die Regeln zu brechen sind.

Jeff Yates
quelle
1
Wie bestimmen Sie die Gleichheit zwischen veränderlichen Typen?
Jon B
9
Sie sollten GetHashCode nicht verwenden, um die Gleichheit zu bestimmen.
JSB 20
4
@JS Bangs - Von MSDN: Abgeleitete Klassen, die GetHashCode überschreiben, müssen auch Equals überschreiben, um sicherzustellen, dass zwei als gleich angesehene Objekte denselben Hashcode haben. Andernfalls funktioniert der Hashtable-Typ möglicherweise nicht richtig.
Jon B
3
@ Joan Venge: Zwei Dinge. Erstens hat nicht einmal Microsoft GetHashCode bei jeder Implementierung richtig gemacht. Zweitens sind Werttypen im Allgemeinen unveränderlich, wobei jeder Wert eher eine neue Instanz als eine Änderung einer vorhandenen Instanz ist.
Jeff Yates
17
Da a.Equals (b) bedeuten muss, dass a.GetHashCode () == b.GetHashCode () ist, muss sich der Hash-Code am häufigsten ändern, wenn die für den Gleichheitsvergleich verwendeten Daten geändert werden. Ich würde sagen, dass das Problem nicht darin besteht, dass GetHashCode auf veränderlichen Daten basiert. Das Problem besteht darin, veränderbare Objekte als Hash-Tabellenschlüssel zu verwenden (und sie tatsächlich zu mutieren). Liege ich falsch?
Niklas
120

Es ist lange her, aber ich denke trotzdem, dass es immer noch notwendig ist, eine korrekte Antwort auf diese Frage zu geben, einschließlich Erklärungen über das Warum und Wie. Die bisher beste Antwort ist die, die den MSDN ausführlich zitiert - versuchen Sie nicht, Ihre eigenen Regeln zu erstellen, die MS-Leute wussten, was sie taten.

Aber das Wichtigste zuerst: Die in der Frage zitierte Richtlinie ist falsch.

Nun das Warum - es gibt zwei von ihnen

Erster Grund : Wenn der Hashcode so berechnet wird, dass er sich während der Lebensdauer eines Objekts nicht ändert, selbst wenn sich das Objekt selbst ändert, würde dies den Gleichstellungsvertrag brechen.

Denken Sie daran: "Wenn zwei Objekte als gleich verglichen werden, muss die GetHashCode-Methode für jedes Objekt denselben Wert zurückgeben. Wenn jedoch zwei Objekte nicht als gleich verglichen werden, müssen die GetHashCode-Methoden für die beiden Objekte keine unterschiedlichen Werte zurückgeben."

Der zweite Satz wird oft falsch interpretiert als "Die einzige Regel ist, dass zum Zeitpunkt der Objekterstellung der Hashcode gleicher Objekte gleich sein muss". Ich weiß nicht genau warum, aber das ist auch hier die Essenz der meisten Antworten.

Stellen Sie sich zwei Objekte vor, die einen Namen enthalten, wobei der Name in der Methode equals verwendet wird: Gleicher Name -> Gleiches. Instanz A erstellen: Name = Joe Instanz B erstellen: Name = Peter

Hashcode A und Hashcode B werden höchstwahrscheinlich nicht identisch sein. Was würde jetzt passieren, wenn der Name von Instanz B in Joe geändert wird?

Gemäß der Richtlinie aus der Frage würde sich der Hashcode von B nicht ändern. Das Ergebnis wäre: A.Equals (B) ==> true, aber gleichzeitig: A.GetHashCode () == B.GetHashCode () ==> false.

Aber genau dieses Verhalten ist im Equals & Hashcode-Vertrag ausdrücklich verboten.

Zweitens warum : Während es natürlich wahr ist, dass Änderungen im Hashcode Hash-Listen und andere Objekte unter Verwendung des Hashcodes beschädigen können, ist das Gegenteil auch der Fall. Wenn Sie den Hashcode nicht ändern, erhalten Sie im schlimmsten Fall Hash-Listen, in denen viele verschiedene Objekte denselben Hashcode haben und sich daher im selben Hash-Bin befinden. Dies geschieht beispielsweise, wenn Objekte mit einem Standardwert initialisiert werden.


Nun zum Hows Nun, auf den ersten Blick scheint es einen Widerspruch zu geben - so oder so wird Code brechen. Kein Problem kommt jedoch von geändertem oder unverändertem Hashcode.

Die Ursache der Probleme ist im MSDN gut beschrieben:

Aus dem Hashtabelleneintrag von MSDN:

Schlüsselobjekte müssen unveränderlich sein, solange sie als Schlüssel in der Hashtabelle verwendet werden.

Dies bedeutet:

Jedes Objekt, das einen Hashwert erstellt, sollte den Hashwert ändern, wenn sich das Objekt ändert, aber es darf keine Änderungen an sich selbst zulassen, wenn es in einer Hashtabelle verwendet wird (oder natürlich in einem anderen Hash-verwendenden Objekt). .

Erstens, wie einfach es natürlich wäre, unveränderliche Objekte nur für die Verwendung in Hashtabellen zu entwerfen, die bei Bedarf als Kopien der normalen, veränderlichen Objekte erstellt werden. Innerhalb der unveränderlichen Objekte ist es offensichtlich in Ordnung, den Hashcode zwischenzuspeichern, da er unveränderlich ist.

Zweitens: Oder geben Sie dem Objekt ein "Sie sind jetzt gehasht" -Flag, stellen Sie sicher, dass alle Objektdaten privat sind, überprüfen Sie das Flag in allen Funktionen, die Objektdaten ändern können, und lösen Sie Ausnahmedaten aus, wenn keine Änderung zulässig ist (dh das Flag ist gesetzt ). Wenn Sie das Objekt jetzt in einem Hash-Bereich platzieren, stellen Sie sicher, dass Sie das Flag setzen und das Flag deaktivieren, wenn es nicht mehr benötigt wird. Aus Gründen der Benutzerfreundlichkeit würde ich empfehlen, das Flag innerhalb der "GetHashCode" -Methode automatisch zu setzen - auf diese Weise kann es nicht vergessen werden. Und der explizite Aufruf einer "ResetHashFlag" -Methode stellt sicher, dass der Programmierer überlegen muss, ob er die Objektdaten jetzt ändern darf oder nicht.

Ok, was auch gesagt werden sollte: Es gibt Fälle, in denen es möglich ist, Objekte mit veränderlichen Daten zu haben, in denen der Hashcode dennoch unverändert bleibt, wenn die Objektdaten geändert werden, ohne den Gleichheits- und Hashcode-Vertrag zu verletzen.

Dies setzt jedoch voraus, dass die Equals-Methode nicht auch auf den veränderlichen Daten basiert. Wenn ich also ein Objekt schreibe und eine GetHashCode-Methode erstelle, die einen Wert nur einmal berechnet und im Objekt speichert, um ihn bei späteren Aufrufen zurückzugeben, muss ich erneut eine Equals-Methode erstellen, die verwendet wird gespeicherte Werte für den Vergleich, so dass A.Equals (B) niemals auch von false zu true wechselt. Andernfalls würde der Vertrag gebrochen. Das Ergebnis davon ist normalerweise, dass die Equals-Methode keinen Sinn ergibt - es ist nicht die ursprüngliche Referenz gleich, aber es ist auch kein Wert gleich. Manchmal kann dies beabsichtigtes Verhalten sein (dh Kundendatensätze), normalerweise jedoch nicht.

Nehmen Sie also einfach das Ergebnis von GetHashCode vor, wenn sich die Objektdaten ändern und wenn die Verwendung des Objekts innerhalb von Hash mithilfe von Listen oder Objekten beabsichtigt (oder nur möglich) ist, machen Sie das Objekt entweder unveränderlich oder erstellen Sie ein schreibgeschütztes Flag, das für das verwendet werden soll Lebensdauer einer Hash-Liste, die das Objekt enthält.

(Übrigens: All dies ist nicht C # oder .NET-spezifisch - es liegt in der Natur aller Hashtable-Implementierungen oder allgemeiner einer indizierten Liste, dass sich die Identifizierungsdaten von Objekten niemals ändern sollten, solange sich das Objekt in der Liste befindet Wenn diese Regel verletzt wird, tritt unerwartetes und unvorhersehbares Verhalten auf. Irgendwo kann es Listenimplementierungen geben, die alle Elemente in der Liste überwachen und die Liste automatisch neu indizieren - aber die Leistung dieser Elemente wird sicherlich bestenfalls grausam sein.)

Alex
quelle
23
+1 für diese detaillierte Erklärung (würde mehr geben, wenn ich könnte)
Oliver
5
+1 Dies ist definitiv die bessere Antwort wegen der ausführlichen Erklärung! :)
Joe
9

Von MSDN

Wenn zwei Objekte gleich sind, muss die GetHashCode-Methode für jedes Objekt denselben Wert zurückgeben. Wenn jedoch zwei Objekte nicht gleich verglichen werden, müssen die GetHashCode-Methoden für die beiden Objekte keine unterschiedlichen Werte zurückgeben.

Die GetHashCode-Methode für ein Objekt muss konsistent denselben Hashcode zurückgeben, solange der Objektstatus, der den Rückgabewert der Equals-Methode des Objekts bestimmt, nicht geändert wird. Beachten Sie, dass dies nur für die aktuelle Ausführung einer Anwendung gilt und dass ein anderer Hash-Code zurückgegeben werden kann, wenn die Anwendung erneut ausgeführt wird.

Für die beste Leistung muss eine Hash-Funktion eine zufällige Verteilung für alle Eingaben generieren.

Dies bedeutet, dass sich der Hash-Code ändern sollte, wenn sich die Werte des Objekts ändern. Beispielsweise sollte eine "Person" -Klasse mit der Eigenschaft "Name" auf "Tom" einen Hashcode und einen anderen Code haben, wenn Sie den Namen in "Jerry" ändern. Ansonsten Tom == Jerry, was wahrscheinlich nicht das ist, was Sie beabsichtigt hätten.


Bearbeiten :

Auch von MSDN:

Abgeleitete Klassen, die GetHashCode überschreiben, müssen auch Equals überschreiben, um sicherzustellen, dass zwei als gleich angesehene Objekte denselben Hashcode haben. Andernfalls funktioniert der Hashtable-Typ möglicherweise nicht richtig.

Aus dem Hashtabelleneintrag von MSDN :

Schlüsselobjekte müssen unveränderlich sein, solange sie als Schlüssel in der Hashtabelle verwendet werden.

Ich habe dies so gelesen, dass veränderbare Objekte unterschiedliche Hashcodes zurückgeben sollten , wenn sich ihre Werte ändern, es sei denn, sie sind für die Verwendung in einer Hashtabelle vorgesehen.

Im Beispiel von System.Drawing.Point ist das Objekt veränderbar und gibt einen anderen Hashcode zurück, wenn sich der X- oder Y-Wert ändert. Dies würde es zu einem schlechten Kandidaten machen, so wie es ist in einer Hashtabelle verwendet zu werden.

Jon B.
quelle
GetHashCode () wurde für die Verwendung in einer Hashtabelle entwickelt, das ist der einzige Punkt dieser Funktion.
Skolima
@skolima - die MSDN-Dokumentation stimmt damit nicht überein. Veränderbare Objekte können GetHashCode () implementieren und sollten unterschiedliche Werte zurückgeben, wenn sich der Wert des Objekts ändert. Hashtables müssen unveränderliche Schlüssel verwenden. Daher können Sie GetHashCode () für etwas anderes als eine Hashtabelle verwenden.
Jon B
9

Ich denke, dass die Dokumentation zu GetHashcode etwas verwirrend ist.

Einerseits gibt MSDN an, dass sich der Hashcode eines Objekts niemals ändern und konstant sein sollte. Andererseits gibt MSDN auch an, dass der Rückgabewert von GetHashcode für 2 Objekte gleich sein sollte, wenn diese 2 Objekte als gleich angesehen werden.

MSDN:

Eine Hash-Funktion muss die folgenden Eigenschaften haben:

  • Wenn zwei Objekte gleich sind, muss die GetHashCode-Methode für jedes Objekt denselben Wert zurückgeben. Wenn jedoch zwei Objekte nicht gleich verglichen werden, müssen die GetHashCode-Methoden für die beiden Objekte keine unterschiedlichen Werte zurückgeben.
  • Die GetHashCode-Methode für ein Objekt muss konsistent denselben Hashcode zurückgeben, solange der Objektstatus, der den Rückgabewert der Equals-Methode des Objekts bestimmt, nicht geändert wird. Beachten Sie, dass dies nur für die aktuelle Ausführung einer Anwendung gilt und dass ein anderer Hash-Code zurückgegeben werden kann, wenn die Anwendung erneut ausgeführt wird.
  • Für die beste Leistung muss eine Hash-Funktion eine zufällige Verteilung für alle Eingaben generieren.

Dies bedeutet dann, dass alle Ihre Objekte unveränderlich sein sollten oder dass die GetHashcode-Methode auf unveränderlichen Eigenschaften Ihres Objekts basieren sollte. Angenommen, Sie haben diese Klasse (naive Implementierung):

public class SomeThing
{
      public string Name {get; set;}

      public override GetHashCode()
      {
          return Name.GetHashcode();
      }

      public override Equals(object other)
      {
           SomeThing = other as Something;
           if( other == null ) return false;
           return this.Name == other.Name;
      }
}

Diese Implementierung verstößt bereits gegen die Regeln, die in MSDN zu finden sind. Angenommen, Sie haben zwei Instanzen dieser Klasse. Die Name-Eigenschaft von Instanz1 wird auf 'Pol' und die Name-Eigenschaft von Instanz2 auf 'Piet' gesetzt. Beide Instanzen geben einen anderen Hashcode zurück und sind auch nicht gleich. Angenommen, ich ändere den Namen von Instanz2 in 'Pol', dann sollten gemäß meiner Equals-Methode beide Instanzen gleich sein und gemäß einer der Regeln von MSDN sollten sie denselben Hashcode zurückgeben.
Dies ist jedoch nicht möglich, da sich der Hashcode von Instanz2 ändert und MSDN angibt, dass dies nicht zulässig ist.

Wenn Sie dann eine Entität haben, können Sie den Hashcode möglicherweise so implementieren, dass er den 'primären Bezeichner' dieser Entität verwendet, der im Idealfall ein Ersatzschlüssel oder eine unveränderliche Eigenschaft ist. Wenn Sie ein Wertobjekt haben, können Sie den Hashcode so implementieren, dass er die 'Eigenschaften' dieses Wertobjekts verwendet. Diese Eigenschaften bilden die 'Definition' des Wertobjekts. Dies ist natürlich die Natur eines Wertobjekts; Sie interessieren sich nicht für die Identität, sondern für den Wert.
Und deshalb sollten Wertobjekte unveränderlich sein. (Genau wie im .NET Framework sind Zeichenfolge, Datum usw. unveränderliche Objekte.)

Eine andere Sache, die mir in den Sinn kommt:
Während welcher 'Sitzung' (ich weiß nicht wirklich, wie ich das nennen soll) sollte 'GetHashCode' einen konstanten Wert zurückgeben. Angenommen, Sie öffnen Ihre Anwendung, laden eine Instanz eines Objekts aus der Datenbank (einer Entität) und erhalten den Hashcode. Es wird eine bestimmte Nummer zurückgegeben. Schließen Sie die Anwendung und laden Sie dieselbe Entität. Ist es erforderlich, dass der Hashcode dieses Mal denselben Wert hat wie beim ersten Laden der Entität? IMHO nicht.

Frederik Gheysels
quelle
1
Ihr Beispiel ist, warum Jeff Yates sagt, dass Sie den Hash-Code nicht auf den veränderlichen Daten basieren können. Sie können ein veränderbares Objekt nicht in ein Wörterbuch einfügen und erwarten, dass es gut funktioniert, wenn der Hash-Code auf den veränderlichen Werten dieses Objekts basiert.
Oger Psalm33
3
Ich kann nicht sehen, wo gegen die MSDN-Regel verstoßen wurde. Die Regel besagt eindeutig: Die GetHashCode-Methode für ein Objekt muss konsistent denselben Hashcode zurückgeben, solange der Objektstatus, der den Rückgabewert der Equals-Methode des Objekts bestimmt, nicht geändert wird . Dies bedeutet , dass hashcode von instance2 darf geändert werden , wenn Sie den Namen instance2 zu Pol ändern
chikak
8

Das ist ein guter Rat. Hier ist, was Brian Pepin zu diesem Thema zu sagen hat:

Das hat mich mehr als einmal gestolpert: Stellen Sie sicher, dass GetHashCode über die Lebensdauer einer Instanz immer den gleichen Wert zurückgibt. Denken Sie daran, dass Hash-Codes in den meisten Hashtabellen-Implementierungen verwendet werden, um "Buckets" zu identifizieren. Wenn sich der "Bucket" eines Objekts ändert, kann eine Hashtabelle Ihr Objekt möglicherweise nicht finden. Dies können sehr schwer zu findende Fehler sein, also machen Sie es gleich beim ersten Mal richtig.

Justin R.
quelle
Ich habe es nicht abgelehnt, aber ich würde vermuten, dass andere es getan haben, weil es ein Zitat ist, das nicht das ganze Problem abdeckt. Stellen Sie sich vor, Zeichenfolgen wären veränderlich, haben aber die Hash-Codes nicht geändert. Sie erstellen "bob", verwenden es als Schlüssel in einer Hashtabelle und ändern dann den Wert in "phil". Als nächstes erstellen Sie eine neue Zeichenfolge "phil". Wenn Sie dann nach einem Hash-Tabelleneintrag mit dem Schlüssel "phil" suchen, wird das ursprünglich eingegebene Element nicht gefunden. Wenn jemand nach "bob" suchen würde, würde es gefunden werden, aber Sie würden einen Wert erhalten, der möglicherweise nicht mehr korrekt ist. Sei entweder fleißig, keine veränderlichen Schlüssel zu verwenden, oder sei dir der Gefahren bewusst.
Eric Tuttleman
@EricTuttleman: Wenn ich die Regeln für ein Framework geschrieben hätte, hätte ich angegeben, dass für jedes Objektpaar Xund Yeinmal X.Equals(Y)oder Y.Equals(X)aufgerufen alle zukünftigen Aufrufe das gleiche Ergebnis liefern sollten. Wenn Sie eine andere Definition von Gleichheit verwenden möchten, verwenden Sie eine EqualityComparer<T>.
Supercat
5

Nicht direkt auf Ihre Frage antworten, aber - wenn Sie Resharper verwenden, vergessen Sie nicht, dass es eine Funktion hat, die eine vernünftige GetHashCode-Implementierung (sowie die Equals-Methode) für Sie generiert. Sie können natürlich angeben, welche Mitglieder der Klasse bei der Berechnung des Hashcodes berücksichtigt werden.

petr k.
quelle
Danke, eigentlich habe ich Resharper nie benutzt, aber ich sehe es immer wieder erwähnt, also sollte ich es versuchen.
Joan Venge
+1 Resharper, falls vorhanden, generiert eine schöne GetHashCode-Implementierung.
ΩmegaMan
5

Schauen Sie sich diesen Blog-Beitrag von Marc Brooks an:

VTOs, RTOs und GetHashCode () - oh mein Gott!

Schauen Sie sich dann den Follow-up-Beitrag an (kann nicht verlinkt werden, da ich neu bin, aber es gibt einen Link im ersten Artikel), der weiter diskutiert und einige kleinere Schwachstellen in der anfänglichen Implementierung abdeckt.

Dies war alles, was ich über das Erstellen einer GetHashCode () - Implementierung wissen musste. Er bietet sogar einen Download seiner Methode zusammen mit einigen anderen Dienstprogrammen an, kurz Gold.

Shaun
quelle
4

Der Hashcode ändert sich nie, aber es ist auch wichtig zu verstehen, woher der Hashcode kommt.

Wenn Ihr Objekt eine Wertesemantik verwendet, dh die Identität des Objekts wird durch seine Werte definiert (wie String, Farbe, alle Strukturen). Wenn die Identität Ihres Objekts von allen Werten unabhängig ist, wird der Hashcode durch eine Teilmenge seiner Werte identifiziert. Beispielsweise wird Ihr StackOverflow-Eintrag irgendwo in einer Datenbank gespeichert. Wenn Sie Ihren Namen oder Ihre E-Mail-Adresse ändern, bleibt Ihr Kundeneintrag unverändert, obwohl sich einige Werte geändert haben (letztendlich werden Sie normalerweise durch eine lange Kunden-ID identifiziert).

Also kurz gesagt:

Werttypsemantik - Hashcode wird durch Werte definiert Referenztypsemantik - Hashcode wird durch eine ID definiert

Ich schlage vor, Sie lesen Domain Driven Design von Eric Evans, in dem er sich mit Entitäten und Werttypen befasst (was mehr oder weniger das ist, was ich oben versucht habe), wenn dies immer noch keinen Sinn ergibt.

DavidN
quelle
Das ist nicht wirklich richtig. Der Hash-Code muss für eine bestimmte Instanz konstant bleiben. Bei Werttypen ist es häufig so, dass jeder Wert eine eindeutige Instanz ist und sich daher der Hash zu ändern scheint, aber tatsächlich eine neue Instanz ist.
Jeff Yates
Sie haben Recht, Werttypen sind unveränderlich, sodass Änderungen nicht möglich sind. Guter Fang.
DavidN