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


1445

Data la seguente classe

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

Ho ignorato il Equalsmetodo perché Foorappresentano una riga per la Footabella s. Qual è il metodo preferito per sovrascrivere il GetHashCode?

Perché è importante eseguire l'override GetHashCode?


36
È importante implementare entrambi uguali e gethashcode, a causa delle collisioni, in particolare durante l'utilizzo dei dizionari. se due oggetti restituiscono lo stesso hashcode, vengono inseriti nel dizionario con concatenamento. Durante l'accesso alla voce viene utilizzato il metodo uguale.
DarthVader,

Risposte:


1320

Sì, è importante se il tuo articolo verrà utilizzato come chiave in un dizionario o HashSet<T>, ecc., Poiché viene utilizzato (in assenza di un'abitudine IEqualityComparer<T>) per raggruppare gli elementi in bucket. Se l'hash-code per due elementi non corrisponde, possono mai essere considerati uguali ( Equals saranno semplicemente mai essere chiamati).

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

  • se due cose sono uguali ( Equals(...) == true), allora devono restituire lo stesso valore perGetHashCode()
  • se GetHashCode()è uguale, è non necessaria per loro di essere la stessa; questa è una collisione e Equalsverrà chiamata per vedere se è una vera uguaglianza o no.

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

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 - per comodità, potresti anche considerare di fornire ==e !=operatori quando esegui l'override Equalse GetHashCode.


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


49
Posso chiederti perché stai moltiplicando con tali fattori?
Leandro López,

22
In realtà, probabilmente potrei perderne uno; il punto è cercare di minimizzare il numero di collisioni - in modo che un oggetto {1,0,0} abbia un hash diverso da {0,1,0} e {0,0,1} (se vedi cosa intendo ),
Marc Gravell

13
Ho modificato i numeri per renderlo più chiaro (e ho aggiunto un seme). Alcuni codici utilizzano numeri diversi, ad esempio il compilatore C # (per tipi anonimi) utilizza un seme di 0x51ed270b e un fattore di -1521134295.
Marc Gravell

76
@Leandro López: di solito i fattori sono scelti come numeri primi perché riduce il numero di collisioni.
Andrei Rînea,

29
"Oh - per comodità, potresti anche considerare di fornire == e! = Operatori quando esegui l'override di Equals e GethashCode.": Microsoft scoraggia l'implementazione dell'operatore == per oggetti che non sono immutabili - msdn.microsoft.com/en-us/library/ ms173147.aspx - "Non è una buona idea sovrascrivere l'operatore == in tipi non immutabili."
antiduh,

137

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

Alla fine ho trovato una soluzione a questo problema mentre lavoravo con NHibernate. Il mio approccio è calcolare il codice hash dall'ID dell'oggetto. L'ID può essere impostato solo tramite il costruttore, quindi se si desidera modificare l'ID, il che è molto improbabile, è necessario creare un nuovo oggetto con un nuovo ID e quindi un nuovo codice hash. Questo approccio funziona meglio con i GUID perché è possibile fornire un costruttore senza parametri che genera in modo casuale un ID.


20
@vanja. Credo che abbia a che fare con: se aggiungi l'oggetto a un dizionario e poi cambi l'ID dell'oggetto, quando lo recuperi in seguito utilizzerai un hash diverso per recuperarlo in modo da non ottenerlo mai dal dizionario.
ANeves,

74
La documentazione di Microsoft relativa alla funzione GetHashCode () non afferma né implica che l'hash dell'oggetto debba rimanere coerente per tutta la sua durata. In realtà, spiega in modo specifico un caso ammissibile in cui potrebbe non essere : "Il metodo GetHashCode per un oggetto deve restituire costantemente lo stesso codice hash purché non vi siano modifiche allo stato dell'oggetto che determini il valore restituito del metodo Equals dell'oggetto ".
PeterAllenWebb,

37
"il codice hash non dovrebbe cambiare durante la vita di un oggetto" - non è vero.
apocalisse

7
Un modo migliore per dire che è "il codice hash (né l'evaulazione di uguale) dovrebbe cambiare durante il periodo in cui l'oggetto viene utilizzato come chiave per una raccolta" Quindi, se aggiungi l'oggetto a un dizionario come chiave, devi assicurarti che GetHashCode e Equals non modificheranno l'output per un determinato input fino a quando non rimuoverai l'oggetto dal dizionario.
Scott Chamberlain,

11
@ScottChamberlain Penso che nel tuo commento NON ti sei dimenticato di NON, dovrebbe essere: "il codice hash (né l'evaulazione di uguali) NON dovrebbe cambiare durante il periodo in cui l'oggetto viene usato come chiave per una collezione". Giusto?
Stan Prokop,

57

Sovrascrivendo Equals stai sostanzialmente affermando di essere quello che sa meglio come confrontare due istanze di un determinato tipo, quindi probabilmente sarai 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, cerca solo di indovinare un buon codice hash basato su tutti i campi della classe, ma poiché conosci il dominio del tuo oggetto o gli intervalli di valori, puoi comunque fornirne uno migliore.


7
Questo non restituirà sempre zero? Probabilmente dovrebbe inizializzare il risultato su 1! Ha anche bisogno di qualche altro punto e virgola.
Sam Mackrill,

16
Sei a conoscenza di ciò che fa l'operatore XOR (^)?
Stephen Drew,

1
Come ho detto, questo è ciò che R # scrive per te (almeno è quello che ha fatto nel 2008) quando gli è stato chiesto. Ovviamente, questo frammento vuole essere modificato in qualche modo dal programmatore. Per quanto riguarda i punti e virgola mancanti ... sì, sembra che li abbia lasciati fuori quando ho copiato e incollato il codice da una selezione di regioni in Visual Studio. Ho anche pensato che le persone avrebbero capito entrambi.
Trappola

3
@SamMackrill Ho aggiunto i punti e virgola mancanti.
Matthew Murdoch,

5
@SamMackrill No, non restituirà sempre 0. 0 ^ a = a, quindi 0 ^ m_someVar1 = m_someVar1. Potrebbe anche impostare il valore iniziale di resulta m_someVar1.
Millie Smith,

41

Si prega di non dimenticare di controllare il parametro obj contro nullquando si esegue l'override Equals(). E confronta anche il tipo.

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

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

    return fooItem.FooId == this.FooId;
}

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


6
Questo controllo per il tipo fallirà nella situazione in cui una sottoclasse si riferisce al metodo Equals della superclasse come parte del proprio confronto (cioè base.Equals (obj)) - dovrebbe usare come invece
sweetfa

@sweetfa: dipende da come viene implementato il metodo Equals della sottoclasse. Potrebbe anche chiamare base.Equals ((BaseType) obj)) che funzionerebbe bene.
huha,

2
No non lo farà: msdn.microsoft.com/en-us/library/system.object.gettype.aspx . Inoltre, l'implementazione di un metodo non dovrebbe fallire o riuscire a seconda del modo in cui viene chiamato. Se il tipo di runtime di un oggetto è una sottoclasse di alcune baseclass, allora Equals () della baseclass dovrebbe restituire true se objeffettivamente è uguale a thisqualunque sia stato chiamato Equals () della baseclass.
Giove,

2
Spostando fooItemin alto e quindi controllandolo per null funzionerà meglio nel caso di null o di un tipo sbagliato.
IllidanS4 vuole che Monica torni il

1
@ 40Alpha Beh, sì, allora non obj as Foosarebbe valido.
IllidanS4 vuole che Monica ritorni il

35

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 :)


1
erm - ma stai restituendo una stringa per un metodo basato su int; _0
jim tollan,

32
No, chiama GetHashCode () dall'oggetto String, che restituisce un int.
Richard Clayton,

3
Non mi aspetto che questo sia veloce come vorrei, non solo per il pugilato coinvolto per i tipi di valore, ma anche per le prestazioni di string.Format. Un altro geek che ho visto è new { prop1, prop2, prop3 }.GetHashCode(). Non posso commentare però quale sarebbe più lento tra questi due. Non abusare degli strumenti.
nawfal,

16
Questo tornerà vero per { prop1="_X", prop2="Y", prop3="Z" }e { prop1="", prop2="X_Y", prop3="Z_" }. Probabilmente non lo vuoi.
Voetsjoeba,

2
Sì, puoi sempre sostituire il simbolo di sottolineatura con qualcosa di non così comune (es. •, ▲, ►, ◄, ☺, ☻) e spero che i tuoi utenti non utilizzino questi simboli ... :)
Ludmil Tinkov,

13

Abbiamo due problemi da affrontare.

  1. Non è possibile fornire un valore ragionevole GetHashCode()se è possibile modificare qualsiasi campo nell'oggetto. Inoltre spesso un oggetto non verrà MAI utilizzato in una raccolta che dipende da GetHashCode(). Quindi il costo di implementazione GetHashCode()spesso non ne vale la pena, o non è possibile.

  2. Se qualcuno inserisce il tuo oggetto in una raccolta che chiama GetHashCode()e hai eseguito l'override Equals()senza anche GetHashCode()comportarti 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;

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

5
Generare un'eccezione da GetHashCode è una violazione del contratto Object. Non è difficile definire una GetHashCodefunzione in modo tale che due oggetti uguali restituiscano lo stesso codice hash; return 24601;e return 8675309;sarebbero entrambe implementazioni valide di GetHashCode. Le prestazioni di Dictionarysaranno decenti solo quando il numero di articoli è piccolo e peggioreranno se il numero di articoli aumenta, ma funzionerà correttamente in ogni caso.
supercat

2
@supercat, Non è possibile implementare GetHashCode in modo ragionevole se i campi di identificazione nell'oggetto possono cambiare, poiché il codice hash non deve mai cambiare. Fare ciò che dici potrebbe portare qualcuno a dover passare molti giorni a rintracciare il problema delle prestazioni, quindi molte settimane su un grande sistema che riprogetta per rimuovere l'uso dei dizionari.
Ian Ringrose,

2
Facevo qualcosa del genere per tutte le classi che avevo definito che avevano bisogno di Equals (), e di cui ero completamente sicuro che non avrei mai usato quell'oggetto come chiave in una collezione. Poi un giorno un programma in cui avevo usato un oggetto del genere come input per un controllo XtraGrid DevExpress si è bloccato. Si scopre che XtraGrid, alle mie spalle, stava creando una HashTable o qualcosa basato sui miei oggetti. Ho avuto un piccolo argomento con il personale di supporto DevExpress su questo. Ho detto che non era intelligente basare la funzionalità e l'affidabilità dei loro componenti su un'implementazione sconosciuta da parte del cliente di un metodo oscuro.
RenniePet,

Le persone DevExpress erano piuttosto snarky, fondamentalmente dicendo che dovevo essere un idiota per lanciare un'eccezione in un metodo GetHashCode (). Penso ancora che dovrebbero trovare un metodo alternativo per fare ciò che stanno facendo - ricordo Marc Gravell su un thread diverso che descrive come costruisce un dizionario di oggetti arbitrari senza dipendere da GetHashCode () - non ricordo come lo abbia fatto anche se.
RenniePet,

4
@RenniePet, è meglio avere una cotta a causa del lancio di un'eccezione, quindi avere un bug molto difficile da trovare a causa di un'implementazione non valida.
Ian Ringrose,

12

È perché il framework richiede che due oggetti uguali debbano avere lo stesso hashcode. Se si sovrascrive il metodo equals per eseguire un confronto speciale di due oggetti e i due oggetti vengono considerati uguali dal metodo, anche il codice hash dei due oggetti deve essere lo stesso. (I dizionari e gli hashtable si basano su questo principio).


11

Solo per aggiungere le risposte sopra:

Se non si sostituisce Equals, il comportamento predefinito è che i riferimenti degli oggetti vengono confrontati. Lo stesso vale per l'hashcode: l'implementazione predefinita si basa in genere su un indirizzo di memoria del riferimento. Dato che hai sovrascritto 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 clienti della tua classe si aspetteranno che l'hashcode abbia una logica simile al metodo equals, ad esempio i metodi linq che usano un IEqualityComparer prima confrontano gli hashcodes e solo se sono uguali confronteranno il metodo Equals () che potrebbe essere più costoso per funzionare, se non abbiamo implementato l'hashcode, l'oggetto uguale avrà probabilmente hashcode diversi (perché hanno un indirizzo di memoria diverso) e sarà determinato erroneamente perché non uguale (Equals () non colpirà nemmeno).

Inoltre, tranne il problema che potresti non essere in grado di trovare l'oggetto se lo hai utilizzato in un dizionario (perché è stato inserito da un hashcode e quando lo cerchi, l'hashcode predefinito sarà probabilmente diverso e, di nuovo, Equals () non verrà nemmeno chiamato, come spiega Marc Gravell 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 supera gli Equals, quindi si vorranno entrambi come chiavi diverse su una struttura di dati che supponga di avere una chiave univoca, ma poiché hanno un codice hash diverso la "stessa" chiave verrà inserita come diversa.


8

Il codice hash viene utilizzato per raccolte basate su hash come Dictionary, Hashtable, HashSet ecc. Lo scopo di questo codice è preordinare molto rapidamente un oggetto specifico inserendolo in un gruppo specifico (bucket). Questo pre-ordinamento aiuta enormemente a trovare questo oggetto quando è necessario recuperarlo dalla raccolta hash perché il codice deve cercare l'oggetto in un solo bucket anziché in tutti gli oggetti che contiene. La migliore distribuzione dei codici hash (migliore unicità) il recupero più veloce. Nella situazione ideale in cui ogni oggetto ha un codice hash univoco, trovarlo è un'operazione O (1). Nella maggior parte dei casi si avvicina a O (1).


7

Non è necessariamente importante; dipende dalle dimensioni delle raccolte e dai requisiti di rendimento e se la classe 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 alcuni microsecondi di prestazioni ottenute creando un codice hash perfetto; quindi (per sbarazzarmi del fastidioso avvertimento del compilatore) uso semplicemente:

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

(Ovviamente potrei usare un #pragma anche per disattivare l'avviso ma preferisco così.)

Quando si è nella posizione che non ha bisogno delle prestazioni di tutti i problemi citati da altri qui si applicano, naturalmente. 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 la durata di vita di un oggetto (più precisamente, durante il tempo in cui è necessario il codice hash, ad esempio una chiave in un dizionario): ad esempio, quanto segue è errato poiché Value è pubblico e quindi può essere modificato esternamente alla classe durante il periodo di 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 il valore non può essere modificato, va bene 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
      }
   }

3
Downvoted. Questo è chiaramente sbagliato. Anche Microsoft afferma in MSDN ( msdn.microsoft.com/en-us/library/system.object.gethashcode.aspx ) che il valore di GetHashCode DEVE cambiare quando lo stato dell'oggetto cambia in un modo che può influenzare il valore restituito di una chiamata to Equals () e anche nei suoi esempi mostra anche implementazioni GetHashCode che dipendono completamente da valori modificabili pubblicamente.
Sebastian PR Gingter,

Sebastian, non sono d'accordo: se aggiungi un oggetto a una raccolta che utilizza codici hash, verrà inserito in un cestino dipendente dal codice hash. Se ora modifichi il codice hash, non troverai di nuovo l'oggetto nella raccolta poiché verrà cercato il cestino sbagliato. Questo è, in effetti, qualcosa che è successo nel nostro codice ed è per questo che ho trovato necessario segnalarlo.
ILoveFortran,

2
Sebastian, inoltre, non riesco a vedere un'istruzione nel collegamento ( msdn.microsoft.com/en-us/library/system.object.gethashcode.aspx ) che GetHashCode () deve cambiare. Al contrario - NON deve cambiare finché Equals restituisce lo stesso valore per lo stesso argomento: "Il metodo GetHashCode per un oggetto deve restituire costantemente lo stesso codice hash purché non vi siano modifiche allo stato dell'oggetto che determinano il valore restituito del metodo Equals dell'oggetto. "Questa affermazione non implica il contrario, che deve cambiare se cambia il valore di ritorno per Equals.
ILoveFortran,

2
@ Joao, stai confondendo il lato cliente / consumatore del contratto con il produttore / implementatore. Sto parlando della responsabilità dell'implementatore, che sostituisce GetHashCode (). Stai parlando del consumatore, colui che sta usando il valore.
ILoveFortran,

1
Completo fraintendimento ... :) La verità è che il codice hash deve cambiare quando lo stato dell'oggetto cambia a meno che lo stato non sia irrilevante per l'identità dell'oggetto. Inoltre, non dovresti mai usare un oggetto MUTABLE come chiave nelle tue raccolte. Utilizzare oggetti di sola lettura per questo scopo. GetHashCode, Equals ... e alcuni altri metodi di cui non ricordo MAI i nomi che non ricordo proprio in questo momento.
darlove l'

0

Dovresti sempre garantire che se due oggetti sono uguali, come definito da Equals (), dovrebbero restituire lo stesso codice hash. Come alcuni altri commenti affermano, in teoria questo non è obbligatorio se l'oggetto non verrà mai utilizzato in un contenitore basato su hash come HashSet o Dictionary. Ti consiglio comunque di seguire sempre questa regola. Il motivo è semplicemente perché è troppo facile per qualcuno cambiare una raccolta da un tipo a un altro con la buona intenzione di migliorare effettivamente le prestazioni o semplicemente trasmettere la semantica del codice in un modo migliore.

Ad esempio, supponiamo di conservare alcuni oggetti in un elenco. Qualche tempo dopo qualcuno si rende conto che un HashSet è un'alternativa molto migliore a causa, ad esempio, delle migliori caratteristiche di ricerca. Questo è quando possiamo metterci nei guai. Elenco userebbe internamente il comparatore di uguaglianza predefinito per il tipo che significa Equals nel tuo caso mentre HashSet utilizza GetHashCode (). Se i due si comportano diversamente, lo sarà anche il tuo programma. E tieni presente che tali problemi non sono i più facili da risolvere.

Ho riassunto questo comportamento con alcune altre insidie ​​di GetHashCode () in a post sul blog in cui è possibile trovare ulteriori esempi e spiegazioni.


0

Di seguito viene mostrato .NET 4.7il metodo preferito di sostituzione GetHashCode(). Se si sceglie come target versioni precedenti di .NET, includere il pacchetto nuget System.ValueTuple .

// C# 7.0+
public override int GetHashCode() => (FooId, FooName).GetHashCode();

In termini di prestazioni, questo metodo supererà la maggior parte delle implementazioni di codice hash composito . Il ValueTuple è structquindi non ci sarà alcun rifiuti, e l'algoritmo sottostante è veloce come ottiene.


-1

Comprendo che GetHashCode originale () restituisce l'indirizzo di memoria dell'oggetto, quindi è essenziale sovrascriverlo se si desidera confrontare due diversi oggetti.

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


-6

Di seguito usare la riflessione mi sembra un'opzione migliore considerando le proprietà pubbliche in quanto con questo non devi preoccuparti dell'aggiunta / rimozione di proprietà (anche se non è uno scenario così comune). Anche questo mi ha permesso di ottenere prestazioni migliori (tempo confrontato con 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;  
    }

12
L'implementazione di GetHashCode () dovrebbe essere molto leggera. Non sono sicuro che l'utilizzo di reflection sia evidente con StopWatch su migliaia di chiamate, ma sicuramente lo è su milioni (pensa di popolare un dizionario da un elenco).
bohdan_trotsenko,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.