Perché è importante sovrascrivere GetHashCode quando il metodo Equals viene sovrascritto?

Data la seguente class

public class Foo { public int FooId { get; set; } public string FooName { get; set; } public override bool Equals(object obj) { Foo fooItem = obj as Foo; return fooItem.FooId == this.FooId; } public override int GetHashCode() { // Which is preferred? return base.GetHashCode(); //return this.FooId.GetHashCode(); } } 

Ho sovrascritto il metodo Equals perché Foo rappresenta una riga per la tabella di Foo . Qual è il metodo preferito per sovrascrivere il GetHashCode ?

Perché è importante sovrascrivere GetHashCode ?

Sì, è importante che il tuo articolo venga usato come chiave in un dizionario, o HashSet , ecc. – poiché viene utilizzato (in assenza di un IEqualityComparer personalizzato IEqualityComparer ) per raggruppare gli articoli in bucket. Se il codice hash per due elementi non corrisponde, essi non possono mai essere considerati uguali ( Equals semplicemente non verrà mai chiamato).

Il metodo GetHashCode() dovrebbe riflettere la logica Equals ; le regole sono:

  • se due cose sono uguali ( Equals(...) == true ) allora devono restituire lo stesso valore per GetHashCode()
  • se GetHashCode() è uguale, non è necessario che siano uguali; questa è una collisione, e Equals sarà chiamato per vedere se è una vera uguaglianza o meno.

In questo caso, sembra ” return FooId; ” è un’implementazione GetHashCode() adatta. Se stai testando più proprietà, è comune combinarle usando il codice come sotto, per ridurre le collisioni diagonali (cioè in modo che il new Foo(3,5) abbia un diverso codice hash su new Foo(5,3) ):

 int hash = 13; hash = (hash * 7) + field1.GetHashCode(); hash = (hash * 7) + field2.GetHashCode(); ... return hash; 

Oh, per comodità, potresti anche considerare di fornire operatori == e != Quando Equals override di Equals e GetHashCode .


Una dimostrazione di ciò che accade quando si sbaglia questo è qui .

In realtà è molto difficile implementare correttamente GetHashCode() perché, oltre alle regole che Marc ha già menzionato, il codice hash non dovrebbe cambiare durante la vita di un object. Pertanto i campi che vengono utilizzati per calcolare il codice hash devono essere immutabili.

Alla fine ho trovato una soluzione a questo problema quando lavoravo con NHibernate. Il mio approccio è quello di calcolare il codice hash dall’ID dell’object. L’ID può essere impostato solo con il costruttore, quindi se vuoi cambiare l’ID, il che è molto improbabile, devi creare un nuovo object che ha un nuovo ID e quindi un nuovo codice hash. Questo approccio funziona meglio con GUID perché è ansible fornire un costruttore senza parametri che genera in modo casuale un ID.

Eseguendo l’override di Equals, stai fondamentalmente affermando che sei quello che sa meglio come confrontare due istanze di un determinato tipo, quindi è probabile che tu sia il miglior candidato per fornire il miglior codice hash.

Questo è un esempio di come ReSharper scrive una funzione GetHashCode () per te:

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

Come puoi vedere, prova a indovinare un buon codice di hash basato su tutti i campi della class, ma dal momento che conosci il dominio o gli intervalli di valori del tuo object, potresti comunque fornirne uno migliore.

Si prega di non dimenticare di controllare il parametro obj su null quando Equals() override di Equals() . E anche confrontare il tipo.

 public override bool Equals(object obj) { if (obj == null || GetType() != obj.GetType()) return false; Foo fooItem = obj as Foo; return fooItem.FooId == this.FooId; } 

Il motivo è: Equals deve restituire false in confronto a null . Vedi anche http://msdn.microsoft.com/en-us/library/bsc2ak47.aspx

Che ne dite di:

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

Supponendo che le prestazioni non siano un problema 🙂

È perché il framework richiede che due oggetti uguali debbano avere lo stesso hashcode. Se si esegue l’override del metodo equals per eseguire un confronto speciale di due oggetti e i due oggetti sono considerati uguali dal metodo, anche il codice hash dei due oggetti deve essere uguale. (Dizionari e Hashtables si basano su questo principio).

Solo per aggiungere le risposte sopra:

Se non si esegue l’override di Equals, il comportamento predefinito è che i riferimenti degli oggetti vengono confrontati. Lo stesso vale per l’hashcode: l’impianto predefinito si basa in genere su un indirizzo di memoria del riferimento. Poiché hai eseguito l’override di Equals, significa che il comportamento corretto è quello di confrontare qualsiasi cosa tu abbia implementato su Equals e non i riferimenti, quindi dovresti fare lo stesso per l’hashcode.

I client della tua class si aspettano che l’hashcode abbia una logica simile al metodo equals, per esempio i metodi linq che usano un IEqualityComparer per prima cosa confrontano gli hashcode e solo se sono uguali essi confronteranno il metodo Equals () che potrebbe essere più costoso per eseguire, se non abbiamo implementato hashcode, l’object uguale avrà probabilmente hashcode diversi (perché hanno un indirizzo di memoria diverso) e verrà determinato erroneamente come non uguale (Equals () non verrà nemmeno colpito).

Inoltre, ad eccezione del problema che potresti non essere in grado di trovare il tuo object se lo hai usato in un dizionario (perché è stato inserito da un hashcode e quando lo cerchi, l’hashcode predefinito sarà probabilmente diverso e ancora Equals () non sarà nemmeno chiamato, come Marc Gravell spiega nella sua risposta, si introduce anche una violazione del dizionario o del concetto di hashset che non dovrebbe consentire chiavi identiche – hai già dichiarato che quegli oggetti sono essenzialmente gli stessi quando si esegue l’override di Equals così si li vogliamo entrambi come chiavi diverse su una struttura dati che suppongono di avere una chiave univoca, ma poiché hanno un diverso codice hash, la “stessa” chiave verrà inserita come una diversa.

Abbiamo due problemi da affrontare.

  1. Non è ansible fornire un GetHashCode() ragionevole se è ansible modificare qualsiasi campo nell’object. Inoltre, spesso un object non verrà MAI utilizzato in una raccolta che dipende da GetHashCode() . Quindi il costo di implementare GetHashCode() spesso non vale la pena, o non è ansible.

  2. Se qualcuno mette il tuo object in una collezione che chiama GetHashCode() e hai Equals() override di Equals() senza fare in modo che GetHashCode() comporti in modo corretto, quella persona potrebbe passare giorni a rintracciare il problema.

Pertanto per impostazione predefinita lo faccio.

 public class Foo { public int FooId { get; set; } public string FooName { get; set; } public override bool Equals(object obj) { Foo fooItem = obj as Foo; 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"); } } 

Il codice hash viene utilizzato per raccolte basate su hash come Dictionary, Hashtable, HashSet ecc. Lo scopo di questo codice è di preordinare molto rapidamente oggetti specifici inserendoli in un gruppo specifico (bucket). Questo pre-ordinamento aiuta moltissimo a trovare questo object quando è necessario recuperarlo da hash-collection perché il codice deve cercare il proprio object in un solo bucket invece che in tutti gli oggetti in esso contenuti. La migliore distribuzione dei codici hash (migliore univocità) il recupero più rapido. Nella situazione ideale in cui ogni object ha un codice hash univoco, trovarlo è un’operazione O (1). Nella maggior parte dei casi si avvicina a O (1).

Non è necessariamente importante; dipende dalla dimensione delle collezioni e dai requisiti di rendimento e dal fatto che la class verrà utilizzata in una libreria in cui potresti non conoscere i requisiti di rendimento. So spesso che le dimensioni della mia collezione non sono molto grandi e il mio tempo è più prezioso di qualche microsecondo di prestazioni ottenute creando un codice hash perfetto; quindi (per eliminare il fastidioso avviso del compilatore) uso semplicemente:

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

(Naturalmente potrei usare un #pragma per distriggersre anche l’avviso, ma preferisco in questo modo).

Ovviamente, quando si è nella posizione in cui si ha bisogno della performance, si applicano tutti i problemi menzionati da altri. La cosa più importante – altrimenti otterrai risultati errati quando recuperi elementi da un set di hash o da un dizionario: il codice hash non deve variare con il tempo di vita di un object (più precisamente, durante il tempo ogni volta che è necessario il codice hash, ad esempio una chiave in un dizionario): ad esempio, il seguente è errato poiché Value è pubblico e quindi può essere modificato esternamente alla class durante la vita dell’istanza, quindi non è necessario utilizzarlo come base per il codice hash:

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

D’altra parte, se Value non può essere cambiato, è ok usare:

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

È a mia conoscenza che l’originale GetHashCode () restituisce l’indirizzo di memoria dell’object, quindi è essenziale sovrascriverlo se si desidera confrontare due oggetti diversi.

EDITED: non era corretto, il metodo originale GetHashCode () non può assicurare l’uguaglianza di 2 valori. Sebbene gli oggetti uguali restituiscano lo stesso codice hash.

Sotto usando la riflessione mi sembra un’opzione migliore considerando le proprietà pubbliche come con questo non devi preoccuparti di aggiunta / rimozione di proprietà (anche se non così scenario comune). Questo mi è sembrato che stia migliorando anche. (Tempo comparato usando il cronometro Diagonistics).

  public int getHashCode() { PropertyInfo[] theProperties = this.GetType().GetProperties(); int hash = 31; foreach (PropertyInfo info in theProperties) { if (info != null) { var value = info.GetValue(this,null); if(value != null) unchecked { hash = 29 * hash ^ value.GetHashCode(); } } } return hash; }