it-swarm.com.de

Warum ist es wichtig, GetHashCode zu überschreiben, wenn die Equals-Methode überschrieben wird?

Gegeben die folgende Klasse

public class Foo
{
    public int FooId { get; set; }
    public string FooName { get; set; }

    public override bool Equals(object obj)
    {
        Foo fooItem = obj as Foo;

        if (fooItem == null) 
        {
           return false;
        }

        return fooItem.FooId == this.FooId;
    }

    public override int GetHashCode()
    {
        // Which is preferred?

        return base.GetHashCode();

        //return this.FooId.GetHashCode();
    }
}

Ich habe die Methode Equals überschrieben, weil Foo eine Zeile für die Tabelle Foo darstellt. Welches ist die bevorzugte Methode zum Überschreiben von GetHashCode?

Warum ist es wichtig, GetHashCode zu überschreiben?

1344
David Basarab

Ja, es ist wichtig, dass Ihr Artikel als Schlüssel in einem Wörterbuch oder als HashSet<T> usw. verwendet wird, da dies verwendet wird (wenn kein benutzerdefinierter IEqualityComparer<T> vorhanden ist), um Artikel in Eimern zu gruppieren. Wenn der Hash-Code für zwei Elemente nicht übereinstimmt, werden sie möglicherweise nie als gleich angesehen (Equals wird einfach nie aufgerufen).

Die Methode GetHashCode() sollte die Logik Equals widerspiegeln. Die Regeln sind:

  • wenn zwei Dinge gleich sind (Equals(...) == true), müssen sie den gleichen Wert für GetHashCode() zurückgeben
  • wenn GetHashCode() gleich ist, ist es nicht erforderlich , dass sie gleich sind; Dies ist eine Kollision, und Equals wird aufgerufen, um festzustellen, ob es sich um eine echte Gleichheit handelt oder nicht.

In diesem Fall sieht es so aus, als wäre "return FooId;" eine geeignete GetHashCode() -Implementierung. Wenn Sie mehrere Eigenschaften testen, ist es üblich, sie mit dem folgenden Code zu kombinieren, um diagonale Kollisionen zu reduzieren (d. H., Dass new Foo(3,5) einen anderen Hash-Code als new Foo(5,3) hat):

unchecked // only needed if you're compiling with arithmetic checks enabled
{ // (the default compiler behaviour is *disabled*, so most folks won't need this)
    int hash = 13;
    hash = (hash * 7) + field1.GetHashCode();
    hash = (hash * 7) + field2.GetHashCode();
    ...
    return hash;
}

Oh - der Einfachheit halber können Sie auch erwägen, die Operatoren == und != anzugeben, wenn Sie Equals und GetHashCode überschreiben.


Eine Demonstration dessen, was passiert, wenn Sie dies falsch verstehen, ist hier .

1238
Marc Gravell

Es ist tatsächlich sehr schwierig, GetHashCode() korrekt zu implementieren, da der Hash-Code zusätzlich zu den bereits erwähnten Regeln von Marc während der Lebensdauer eines Objekts nicht geändert werden sollte. Daher müssen die Felder, die zur Berechnung des Hash-Codes verwendet werden, unveränderlich sein.

Als ich mit NHibernate zusammengearbeitet habe, habe ich endlich eine Lösung für dieses Problem gefunden. Mein Ansatz ist es, den Hash-Code aus der ID des Objekts zu berechnen. Die ID kann nur über den Konstruktor festgelegt werden. Wenn Sie also die ID ändern möchten, was sehr unwahrscheinlich ist, müssen Sie ein neues Objekt mit einer neuen ID und daher einem neuen Hashcode erstellen. Dieser Ansatz funktioniert am besten mit GUIDs, da Sie einen parameterlosen Konstruktor bereitstellen können, der zufällig eine ID generiert.

129
Albic

Indem Sie Equals überschreiben, geben Sie im Grunde genommen an, dass Sie derjenige sind, der besser weiß, wie zwei Instanzen eines bestimmten Typs verglichen werden, sodass Sie wahrscheinlich der beste Kandidat für die Bereitstellung des besten Hash-Codes sind.

Dies ist ein Beispiel dafür, wie ReSharper eine GetHashCode () -Funktion für Sie schreibt:

public override int GetHashCode()
{
    unchecked
    {
        var result = 0;
        result = (result * 397) ^ m_someVar1;
        result = (result * 397) ^ m_someVar2;
        result = (result * 397) ^ m_someVar3;
        result = (result * 397) ^ m_someVar4;
        return result;
    }
}

Wie Sie sehen, wird nur versucht, einen guten Hash-Code basierend auf allen Feldern in der Klasse zu erraten. Da Sie jedoch die Domäne oder die Wertebereiche Ihres Objekts kennen, können Sie dennoch einen besseren Code bereitstellen.

53
Trap

Vergessen Sie bitte nicht, den Parameter obj mit null zu vergleichen, wenn Sie Equals() überschreiben. Und vergleichen Sie auch den Typ.

public override bool Equals(object obj)
{
    Foo fooItem = obj as Foo;

    if (fooItem == null)
    {
       return false;
    }

    return fooItem.FooId == this.FooId;
}

Der Grund dafür ist: Equals muss im Vergleich zu null false zurückgeben. Siehe auch http://msdn.Microsoft.com/en-us/library/bsc2ak47.aspx

40
huha

Wie wäre es mit:

public override int GetHashCode()
{
    return string.Format("{0}_{1}_{2}", prop1, prop2, prop3).GetHashCode();
}

Vorausgesetzt, die Leistung ist kein Problem :)

31
Ludmil Tinkov

Wir haben zwei Probleme zu bewältigen.

  1. Sie können kein sinnvolles GetHashCode() angeben, wenn ein Feld im Objekt geändert werden kann. Außerdem wird ein Objekt NIEMALS in einer Sammlung verwendet, die von GetHashCode() abhängt. Daher sind die Kosten für die Implementierung von GetHashCode() oft nicht wert, oder es ist nicht möglich.

  2. Wenn jemand Ihr Objekt in eine Sammlung einfügt, die GetHashCode() aufruft und Sie Equals() überschrieben haben, ohne dass sich GetHashCode() korrekt verhält, kann diese Person Tage damit verbringen, das Problem aufzuspüren.

Deshalb tue ich das standardmäßig.

public class Foo
{
    public int FooId { get; set; }
    public string FooName { get; set; }

    public override bool Equals(object obj)
    {
        Foo fooItem = obj as Foo;

        if (fooItem == null)
        {
           return false;
        }

        return fooItem.FooId == this.FooId;
    }

    public override int GetHashCode()
    {
        // Some comment to explain if there is a real problem with providing GetHashCode() 
        // or if I just don't see a need for it for the given class
        throw new Exception("Sorry I don't know what GetHashCode should do for this class");
    }
}
12
Ian Ringrose

Nur um die obigen Antworten zu ergänzen:

Wenn Sie Equals nicht überschreiben, werden standardmäßig die Referenzen der Objekte verglichen. Gleiches gilt für Hashcode - die Standardimplementierung basiert normalerweise auf einer Speicheradresse der Referenz. Da Sie Equals überschrieben haben, bedeutet dies, dass das korrekte Verhalten darin besteht, alles zu vergleichen, was Sie in Equals implementiert haben, und nicht die Referenzen. Daher sollten Sie dasselbe für den Hashcode tun.

Clients Ihrer Klasse erwarten, dass der Hashcode eine ähnliche Logik wie die equals-Methode aufweist. Beispielsweise vergleichen Linq-Methoden, die einen IEqualityComparer verwenden, zuerst die Hashcodes und nur dann, wenn sie gleich sind, die möglicherweise teurere Equals () -Methode Wenn wir keinen Hashcode implementiert haben, hat das gleiche Objekt wahrscheinlich unterschiedliche Hashcodes (weil sie unterschiedliche Speicheradressen haben) und wird fälschlicherweise als ungleich bestimmt (Equals () trifft nicht einmal).

Abgesehen von dem Problem, dass Sie Ihr Objekt möglicherweise nicht finden können, wenn Sie es in einem Wörterbuch verwendet haben (da es durch einen Hashcode eingefügt wurde und der Standard-Hashcode bei der Suche danach wahrscheinlich anders ist), ist auch hier Equals (). wird nicht einmal aufgerufen, wie Marc Gravell in seiner Antwort erklärt, sondern Sie führen auch eine Verletzung des Wörterbuch- oder Hashset-Konzepts ein, die keine identischen Schlüssel zulassen sollte Sie möchten nicht, dass beide Schlüssel in einer Datenstruktur einen eindeutigen Schlüssel haben, aber weil sie einen unterschiedlichen Hashcode haben, wird der "gleiche" Schlüssel als ein anderer eingefügt.

10
BornToCode

Dies liegt daran, dass das Framework erfordert, dass zwei Objekte, die identisch sind, denselben Hashcode haben müssen. Wenn Sie die Methode equals überschreiben, um einen speziellen Vergleich zweier Objekte durchzuführen, und die beiden Objekte von der Methode als gleich angesehen werden, muss auch der Hash-Code der beiden Objekte identisch sein. (Wörterbücher und Hashtables basieren auf diesem Prinzip).

10
kemiller2002

Hash-Code wird für Hash-basierte Sammlungen wie Dictionary, Hashtable, HashSet usw. verwendet. Der Zweck dieses Codes besteht darin, ein bestimmtes Objekt sehr schnell vorsortieren zu können, indem es in eine bestimmte Gruppe (Bucket) eingeteilt wird. Diese Vorsortierung hilft enorm, dieses Objekt zu finden, wenn Sie es aus der Hash-Sammlung zurückholen müssen, da der Code in nur einem Bucket nach Ihrem Objekt suchen muss, anstatt in allen Objekten, die es enthält. Je besser die Verteilung der Hash-Codes (bessere Eindeutigkeit) desto schneller der Abruf. In einer idealen Situation, in der jedes Objekt einen eindeutigen Hashcode hat, ist das Auffinden eine O(1) -Operation. In den meisten Fällen nähert es sich O (1).

8
Maciej

Es ist nicht unbedingt wichtig; Dies hängt von der Größe Ihrer Sammlungen und Ihren Leistungsanforderungen ab und davon, ob Ihre Klasse in einer Bibliothek verwendet wird, in der Sie die Leistungsanforderungen möglicherweise nicht kennen. Ich weiß häufig, dass meine Sammlung nicht sehr groß ist und meine Zeit mehr wert ist als ein paar Mikrosekunden Leistung, die durch die Erstellung eines perfekten Hash-Codes erzielt wird. Also (um die nervige Warnung des Compilers loszuwerden) benutze ich einfach:

   public override int GetHashCode()
   {
      return base.GetHashCode();
   }

(Natürlich könnte ich auch ein #pragma verwenden, um die Warnung auszuschalten, aber ich bevorzuge diesen Weg.)

Wenn Sie in der Position sind, dass Sie die Leistung benötigen , dann gelten natürlich alle von anderen hier erwähnten Punkte. Das Wichtigste - Andernfalls erhalten Sie falsche Ergebnisse beim Abrufen von Elementen aus einem Hash-Set oder Wörterbuch: Der Hash-Code darf nicht mit der Lebensdauer eines Objekts variieren. (Genauer gesagt, Während der Zeit, in der der Hash-Code benötigt wird, z. B. als Schlüssel in einem Wörterbuch Sie dürfen es nicht als Grundlage für den Hash-Code verwenden:


   class A
   {
      public int Value;

      public override int GetHashCode()
      {
         return Value.GetHashCode(); //WRONG! Value is not constant during the instance's life time
      }
   }    

Wenn der Wert jedoch nicht geändert werden kann, kann Folgendes verwendet werden:


   class A
   {
      public readonly int Value;

      public override int GetHashCode()
      {
         return Value.GetHashCode(); //OK  Value is read-only and can't be changed during the instance's life time
      }
   }

6
ILoveFortran

Ich verstehe, dass das ursprüngliche GetHashCode () die Speicheradresse des Objekts zurückgibt, daher ist es wichtig, diese zu überschreiben, wenn Sie zwei verschiedene Objekte vergleichen möchten.

BEARBEITET: Das war falsch, die ursprüngliche GetHashCode () -Methode kann nicht die Gleichheit von 2 Werten sicherstellen. Gleiche Objekte geben jedoch denselben Hash-Code zurück.

0
user2855602