Linee guida GetHashCode in C #


136

Ho letto nel libro Essential C # 3.0 e .NET 3.5 che:

I rendimenti di GetHashCode () nel corso della vita di un particolare oggetto devono essere costanti (lo stesso valore), anche se i dati dell'oggetto cambiano. In molti casi, è necessario memorizzare nella cache il metodo return per applicarlo.

È una linea guida valida?

Ho provato un paio di tipi predefiniti in .NET e non si sono comportati così.


Si consiglia di modificare la risposta accettata, se possibile.
Giffyguy,

Risposte:


93

La risposta è principalmente, è una linea guida valida, ma forse non una regola valida. Inoltre non racconta tutta la storia.

Il punto in questione è che per i tipi mutabili non è possibile basare il codice hash sui dati mutabili perché due oggetti uguali devono restituire lo stesso codice hash e il codice hash deve essere valido per la durata dell'oggetto. Se il codice hash cambia, si finisce con un oggetto che si perde in una raccolta hash perché non vive più nel cestino hash corretto.

Ad esempio, l'oggetto A restituisce un hash di 1. Quindi, va nel cestino 1 della tabella hash. Quindi si modifica l'oggetto A in modo che restituisca un hash di 2. Quando una tabella hash va cercandolo, cerca nel cestino 2 e non riesce a trovarlo - l'oggetto è rimasto orfano nel cestino 1. Ecco perché il codice hash deve non cambia per la durata dell'oggetto e solo uno dei motivi per cui scrivere le implementazioni di GetHashCode è una seccatura.

Aggiornamento
Eric Lippert ha pubblicato un blog che fornisce informazioni eccellenti su GetHashCode.

Aggiornamento aggiuntivo
Ho apportato un paio di modifiche sopra:

  1. Ho fatto una distinzione tra linea guida e regola.
  2. Ho attraversato "per la durata dell'oggetto".

Una linea guida è solo una guida, non una regola. In realtà, GetHashCodedeve seguire queste linee guida solo quando le cose si aspettano che l'oggetto segua le linee guida, come quando viene memorizzato in una tabella hash. Se non hai mai intenzione di usare i tuoi oggetti nelle tabelle hash (o qualsiasi altra cosa che si basi sulle regole di GetHashCode), l'implementazione non deve seguire le linee guida.

Quando vedi "per la durata dell'oggetto", dovresti leggere "per il tempo in cui l'oggetto deve cooperare con le tabelle hash" o simili. Come la maggior parte delle cose, si GetHashCodetratta di sapere quando infrangere le regole.


1
Come si determina l'uguaglianza tra i tipi mutabili?
Jon B,

9
Non dovresti usare GetHashCode per determinare l'uguaglianza.
JSB ձոգչ

4
@JS Bangs - Da MSDN: le classi derivate che sovrascrivono GetHashCode devono anche sostituire Equals per garantire che due oggetti considerati uguali abbiano lo stesso codice hash; in caso contrario, il tipo Hashtable potrebbe non funzionare correttamente.
Jon B,

3
@Joan Venge: due cose. Innanzitutto, nemmeno Microsoft ha GetHashCode giusto in ogni implementazione. In secondo luogo, i tipi di valore sono generalmente immutabili con ogni valore che è una nuova istanza anziché una modifica di un'istanza esistente.
Jeff Yates,

17
Poiché a.Equals (b) deve significare che a.GetHashCode () == b.GetHashCode (), il codice hash molto spesso deve cambiare se i dati utilizzati per il confronto di uguaglianza sono cambiati. Direi che il problema non è che GetHashCode sia basato su dati mutabili. Il problema sta usando gli oggetti mutabili come chiavi della tabella hash (e li sta effettivamente mutando). Ho sbagliato?
Niklas,

120

È passato molto tempo, ma penso comunque che sia ancora necessario dare una risposta corretta a questa domanda, comprese le spiegazioni sui perché e come. La risposta migliore finora è quella che cita esaustivamente l'MSDN - non provare a stabilire le tue regole, i ragazzi della MS sapevano cosa stavano facendo.

Ma prima le cose: la linea guida citata nella domanda è sbagliata.

Ora i perché - ce ne sono due

Primo perché : se l'hashcode viene calcolato in un modo, non cambia durante la vita di un oggetto, anche se l'oggetto stesso cambia, di quanto non si romperà il contratto uguale.

Ricorda: "Se due oggetti si confrontano come uguali, il metodo GetHashCode per ogni oggetto deve restituire lo stesso valore. Tuttavia, se due oggetti non si confrontano come uguali, i metodi GetHashCode per i due oggetti non devono restituire valori diversi."

La seconda frase viene spesso interpretata erroneamente come "L'unica regola è che al momento della creazione dell'oggetto, l'hashcode di oggetti uguali deve essere uguale". Non so davvero perché, ma qui si trova anche l'essenza della maggior parte delle risposte.

Pensa a due oggetti contenenti un nome, in cui il nome viene utilizzato nel metodo uguale: Stesso nome -> stessa cosa. Crea istanza A: Nome = Joe Crea istanza B: Nome = Pietro

Hashcode A e Hashcode B molto probabilmente non saranno gli stessi. Cosa succederebbe ora, quando il Nome dell'istanza B viene cambiato in Joe?

Secondo le linee guida della domanda, l'hashcode di B non cambierebbe. Il risultato sarebbe: A.Equals (B) ==> true Ma allo stesso tempo: A.GetHashCode () == B.GetHashCode () ==> false.

Ma esattamente questo comportamento è esplicitamente vietato dal contratto uguale e hashcode.

Secondo perché : mentre è - ovviamente - vero, che i cambiamenti nell'hashcode potrebbero rompere gli elenchi hash e altri oggetti usando l'hashcode, anche il contrario è vero. Se non si modifica l'hashcode, nel peggiore dei casi si otterranno elenchi di hash, in cui molti oggetti diversi avranno lo stesso hashcode e quindi si troveranno nello stesso hash bin, ad esempio quando gli oggetti vengono inizializzati con un valore standard.


Ora veniamo ai campi Bene, a prima vista sembra esserci una contraddizione: in entrambi i casi, il codice si romperà. Ma nessuno dei due problemi proviene da hashcode modificato o invariato.

La fonte dei problemi è ben descritta in MSDN:

Dalla voce hashtable di MSDN:

Gli oggetti chiave devono essere immutabili purché siano utilizzati come chiavi nella Hashtable.

Questo significa:

Qualsiasi oggetto che crea un valore hash deve cambiare l'hashvalue, quando l'oggetto cambia, ma non deve - assolutamente non deve - consentire eventuali modifiche a se stesso, quando viene utilizzato all'interno di un Hashtable (o qualsiasi altro oggetto che utilizza Hash, ovviamente) .

Innanzitutto come sarebbe ovviamente il modo più semplice per progettare oggetti immutabili solo per l'uso in hashtable, che verranno creati come copie degli oggetti normali, mutabili quando necessario. All'interno degli oggetti immutabili, è ovviamente ok memorizzare nella cache l'hashcode, poiché è immutabile.

Secondo come O assegnare all'oggetto un "hash hashed now" -flag, assicurarsi che tutti i dati degli oggetti siano privati, controllare il flag in tutte le funzioni che possono cambiare i dati degli oggetti e lanciare i dati di un'eccezione se la modifica non è consentita (cioè il flag è impostato ). Ora, quando metti l'oggetto in qualsiasi area con hash, assicurati di impostare la bandiera e - anche - disinserire la bandiera, quando non è più necessaria. Per facilità d'uso, consiglierei di impostare automaticamente il flag all'interno del metodo "GetHashCode" - in questo modo non può essere dimenticato. E la chiamata esplicita di un metodo "ResetHashFlag" farà in modo che il programmatore debba pensare, sia che sia o non sia autorizzato a modificare i dati degli oggetti ormai.

Ok, cosa si dovrebbe dire anche: ci sono casi in cui è possibile avere oggetti con dati mutabili, in cui l'hashcode è comunque invariato, quando i dati degli oggetti vengono modificati, senza violare il contratto uguale e hashcode.

Ciò richiede tuttavia che il metodo uguale non si basi anche sui dati mutabili. Quindi, se scrivo un oggetto e creo un metodo GetHashCode che calcola un valore una sola volta e lo memorizza all'interno dell'oggetto per restituirlo in chiamate successive, allora devo, ancora: assolutamente, creare un metodo Equals, che utilizzerà valori memorizzati per il confronto, in modo che A.Equals (B) non cambierà mai da falso a vero. Altrimenti, il contratto sarebbe rotto. Il risultato di questo sarà di solito che il metodo Equals non ha alcun senso - non è il riferimento originale uguale, ma non è neppure un valore uguale. A volte, questo può essere un comportamento previsto (ovvero i registri dei clienti), ma di solito non lo è.

Quindi, fai semplicemente cambiare il risultato GetHashCode, quando cambiano i dati degli oggetti, e se è previsto (o solo possibile) l'uso dell'oggetto all'interno dell'hash usando liste o oggetti, allora rendi l'oggetto immutabile o crea un flag di sola lettura da usare per il durata di un elenco con hash contenente l'oggetto.

(A proposito: tutto ciò non è specifico per C # o .NET. È nella natura di tutte le implementazioni hashtable, o più in generale di qualsiasi elenco indicizzato, che l'identificazione dei dati degli oggetti non dovrebbe mai cambiare, mentre l'oggetto è nell'elenco Se si verifica una violazione di questa regola, si verificherà un comportamento imprevisto e imprevedibile. Da qualche parte, potrebbero esserci implementazioni di elenchi che monitorano tutti gli elementi all'interno dell'elenco e reindicizzano automaticamente l'elenco, ma le prestazioni di questi saranno sicuramente alquanto raccapriccianti.)


23
+1 per questa spiegazione dettagliata (darei di più se potessi)
Oliver,

5
+1 questa è sicuramente la risposta migliore a causa della spiegazione dettagliata! :)
Joe,

9

Da MSDN

Se due oggetti si equivalgono, il metodo GetHashCode per ciascun oggetto deve restituire lo stesso valore. Tuttavia, se due oggetti non vengono confrontati come uguali, i metodi GetHashCode per i due oggetti non devono restituire valori diversi.

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. Si noti che ciò vale solo per l'esecuzione corrente di un'applicazione e che un codice hash diverso può essere restituito se l'applicazione viene eseguita nuovamente.

Per prestazioni ottimali, una funzione hash deve generare una distribuzione casuale per tutti gli input.

Ciò significa che se i valori dell'oggetto cambiano, il codice hash dovrebbe cambiare. Ad esempio, una classe "Person" con la proprietà "Name" impostata su "Tom" dovrebbe avere un codice hash e un codice diverso se si modifica il nome in "Jerry". Altrimenti, Tom == Jerry, che probabilmente non è quello che avresti voluto.


Modifica :

Anche da MSDN:

Le classi derivate che sovrascrivono GetHashCode devono anche sostituire Equals per garantire che due oggetti considerati uguali abbiano lo stesso codice hash; in caso contrario, il tipo Hashtable potrebbe non funzionare correttamente.

Dalla voce hashtable di MSDN :

Gli oggetti chiave devono essere immutabili purché siano utilizzati come chiavi nella Hashtable.

Il modo in cui leggo questo è che gli oggetti mutabili dovrebbero restituire hashcode diversi quando cambiano i loro valori, a meno che non siano progettati per l'uso in una tabella hash.

Nell'esempio di System.Drawing.Point, l'oggetto è mutevole, e fa ritornare un codice hash diverso quando la X o Y valore cambia. Ciò renderebbe un candidato scarso essere utilizzato così com'è in una tabella hash.


GetHashCode () è progettato per l'uso in una tabella hash, questo è l'unico punto di questa funzione.
skolima,

@skolima - la documentazione MSDN non è coerente con quella. Gli oggetti mutabili possono implementare GetHashCode () e dovrebbero restituire valori diversi quando il valore dell'oggetto cambia. Gli hashtable devono usare chiavi immutabili. Quindi, puoi usare GetHashCode () per qualcosa di diverso da una tabella hash.
Jon B,

9

Penso che la documentazione relativa a GetHashcode sia un po 'confusa.

Da un lato, MSDN afferma che l'hashcode di un oggetto non dovrebbe mai cambiare, ed essere costante D'altro canto, MSDN afferma anche che il valore di ritorno di GetHashcode dovrebbe essere uguale per 2 oggetti, se questi 2 oggetti sono considerati uguali.

MSDN:

Una funzione hash deve avere le seguenti proprietà:

  • Se due oggetti si equivalgono, il metodo GetHashCode per ciascun oggetto deve restituire lo stesso valore. Tuttavia, se due oggetti non vengono confrontati come uguali, i metodi GetHashCode per i due oggetti non devono restituire valori diversi.
  • 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. Si noti che ciò vale solo per l'esecuzione corrente di un'applicazione e che un codice hash diverso può essere restituito se l'applicazione viene eseguita nuovamente.
  • Per prestazioni ottimali, una funzione hash deve generare una distribuzione casuale per tutti gli input.

Quindi, ciò significa che tutti i tuoi oggetti dovrebbero essere immutabili o il metodo GetHashcode dovrebbe essere basato su proprietà del tuo oggetto immutabili. Supponiamo ad esempio di avere questa classe (implementazione ingenua):

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

Questa implementazione viola già le regole che si possono trovare in MSDN. Supponiamo di avere 2 istanze di questa classe; la proprietà Name di istanza1 è impostata su "Pol" e la proprietà Name di istanza2 è impostata su "Piet". Entrambe le istanze restituiscono un hashcode diverso e non sono uguali. Supponiamo ora che cambi il Nome dell'istanza2 in "Pol", quindi, secondo il mio metodo Equals, entrambe le istanze dovrebbero essere uguali e, secondo una delle regole di MSDN, dovrebbero restituire lo stesso hashcode.
Tuttavia, ciò non può essere fatto, poiché il codice hash di instance2 cambierà e MSDN afferma che ciò non è consentito.

Quindi, se hai un'entità, potresti forse implementare l'hashcode in modo che utilizzi l'identificatore principale di quell'entità, che è forse idealmente una chiave surrogata o una proprietà immutabile. Se si dispone di un oggetto valore, è possibile implementare l'Hashcode in modo che utilizzi le "proprietà" dell'oggetto valore. Tali proprietà costituiscono la "definizione" dell'oggetto valore. Questa è ovviamente la natura di un oggetto valore; non ti interessa la sua identità, ma piuttosto il suo valore.
E, quindi, gli oggetti valore dovrebbero essere immutabili. (Proprio come nel framework .NET, string, Date, ecc ... sono tutti oggetti immutabili).

Un'altra cosa che viene in mente:
durante la quale 'sessione' (non so davvero come dovrei chiamarlo) dovrebbe 'GetHashCode' restituire un valore costante. Supponiamo di aprire l'applicazione, caricare un'istanza di un oggetto dal DB (un'entità) e ottenere il suo codice hash. Restituirà un certo numero. Chiudi l'applicazione e carica la stessa entità. È necessario che l'hashcode questa volta abbia lo stesso valore di quando hai caricato l'entità la prima volta? IMHO, no.


1
Il tuo esempio è il motivo per cui Jeff Yates afferma che non puoi basare il codice hash sui dati mutabili. Non è possibile inserire un oggetto mutabile in un dizionario e aspettarsi che funzioni bene se il codice hash si basa sui valori mutabili di quell'oggetto.
Ogre Salmo33

3
Non riesco a vedere dove viene violata la regola MSDN? La regola dice chiaramente: il metodo GetHashCode per un oggetto deve costantemente restituire lo stesso codice hash purché non vi siano modifiche allo stato dell'oggetto che determini il valore di ritorno del metodo Equals dell'oggetto . Ciò significa che l'hashcode dell'istanza2 può essere modificato quando si cambia il nome dell'istanza2 in Pol
chikak

8

Questo è un buon consiglio Ecco cosa ha da dire Brian Pepin in merito:

Questo mi ha fatto scattare più di una volta: assicurati che GetHashCode restituisca sempre lo stesso valore per tutta la durata di un'istanza. Ricorda che i codici hash vengono utilizzati per identificare i "bucket" nella maggior parte delle implementazioni hashtable. Se il "secchio" di un oggetto cambia, una tabella hash potrebbe non essere in grado di trovare l'oggetto. Questi possono essere dei bug molto difficili da trovare, quindi fallo bene la prima volta.


Non ho votato per difetto, ma immagino che altri lo abbiano fatto perché è una citazione che non copre l'intero problema. Le stringhe di finzione erano mutabili, ma non cambiarono i codici hash. Crei "bob", lo usi come chiave in una tabella hash e poi cambi il suo valore in "phil". Quindi creare una nuova stringa "phil". se cerchi una voce della tabella hash con la chiave "phil", l'elemento che hai inserito inizialmente non verrà trovato. Se qualcuno cercasse "bob" verrebbe trovato, ma otterresti un valore che potrebbe non essere più corretto. O sii diligente nel non usare chiavi mutabili o fai attenzione ai pericoli.
Eric Tuttleman,

@EricTuttleman: Se stessi scrivendo le regole per un framework, avrei specificato che per qualsiasi coppia di oggetti Xe Y, una volta X.Equals(Y)o Y.Equals(X)è stato chiamato, tutte le chiamate future dovrebbero produrre lo stesso risultato. Se si desidera utilizzare un'altra definizione di uguaglianza, utilizzare un EqualityComparer<T>.
supercat

5

Non rispondere direttamente alla tua domanda, ma - se usi Resharper, non dimenticare che ha una funzione che genera un'implementazione GetHashCode ragionevole (così come il metodo Equals) per te. Ovviamente puoi specificare quali membri della classe saranno presi in considerazione durante il calcolo dell'hashcode.


Grazie, in realtà non ho mai usato Resharper ma continuo a vederlo menzionato abbastanza spesso, quindi dovrei provarlo.
Joan Venge,

+1 Resharper se ne ha uno genera una buona implementazione GetHashCode.
ΩmegaMan del

5

Dai un'occhiata a questo post sul blog di Marc Brooks:

VTO, RTO e GetHashCode () - oh, mio!

E poi dai un'occhiata al post di follow-up (non posso collegarmi come sono nuovo, ma c'è un link nell'articolo iniziale) che discute ulteriormente e copre alcuni punti deboli nell'implementazione iniziale.

Questo era tutto ciò che dovevo sapere sulla creazione di un'implementazione GetHashCode (), fornisce anche un download del suo metodo insieme ad altre utilità, in breve oro.


4

L'hashcode non cambia mai, ma è anche importante capire da dove proviene l'Hashcode.

Se il tuo oggetto utilizza la semantica del valore, ovvero l'identità dell'oggetto è definita dai suoi valori (come String, Color, tutte le strutture). Se l'identità del tuo oggetto è indipendente da tutti i suoi valori, l'Hashcode viene identificato da un sottoinsieme dei suoi valori. Ad esempio, la voce StackOverflow è archiviata in un database da qualche parte. Se cambi il tuo nome o e-mail, la voce del cliente rimane invariata, anche se alcuni valori sono cambiati (alla fine sei di solito identificato da un ID cliente lungo).

Quindi in breve:

Semantica del tipo di valore: il codice hash è definito da valori Semantica del tipo di riferimento: il codice hash è definito da un ID

Ti suggerisco di leggere Domain Driven Design di Eric Evans, dove entra in entità vs tipi di valore (che è più o meno ciò che ho tentato di fare sopra) se questo non ha ancora senso.


Questo non è proprio corretto. Il codice hash deve rimanere costante per una particolare istanza. Nel caso dei tipi di valore, spesso accade che ogni valore sia un'istanza univoca e pertanto l'hash sembra cambiare, ma in realtà è una nuova istanza.
Jeff Yates,

Hai ragione, i tipi di valore sono immutabili e quindi precludono il cambiamento. Buona pesca.
DavidN,

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.