Override di GetHashCode per oggetti mutabili?

Ho letto circa 10 domande diverse su quando e come sovrascrivere GetHashCode ma c’è ancora qualcosa che non riesco a capire. La maggior parte delle implementazioni di GetHashCode sono basate sui codici hash dei campi dell’object, ma è stato citato che il valore di GetHashCode non dovrebbe mai cambiare durante la vita dell’object. Come funziona se i campi su cui si basa sono mutabili? Inoltre, cosa succede se voglio che le ricerche nel dizionario, ecc., Siano basate sull’uguaglianza di riferimento e non sui miei Equals sovrascritti?

Sto principalmente sovrascrivendo Equals per la facilità di testare il mio codice di serializzazione che presumo di serializzare e deserializzare (in XML nel mio caso) uccide l’uguaglianza di riferimento, quindi voglio essere sicuro che sia corretto in base all’uguaglianza di valore. In questo caso, questa ctriggers pratica ha la precedenza su Equals ? Fondamentalmente nella maggior parte del codice in esecuzione voglio uguaglianza di riferimento e uso sempre == e non lo sovrascrivo. Dovrei semplicemente creare un nuovo metodo ValueEquals o qualcosa invece di ValueEquals override di Equals ? Ero solito presumere che il framework usasse sempre == e non fosse Equals a confrontare le cose e quindi pensavo che fosse sicuro sovrascrivere gli Equals poiché mi sembrava che il suo scopo fosse se volessi avere una seconda definizione di uguaglianza che è diversa da l’operatore == . Dalla lettura di molte altre domande sembra che non sia così.

MODIFICARE:

Sembra che le mie intenzioni non fossero chiare, ciò che intendo è che il 99% delle volte voglio la semplice uguaglianza di riferimento, il comportamento predefinito, nessuna sorpresa. Per casi molto rari voglio avere equità di valore, e voglio richiedere esplicitamente l’uguaglianza di valore usando .Equals anziché == .

Quando faccio questo il compilatore consiglia di sovrascrivere anche GetHashCode , ed è così che è venuta fuori questa domanda. Sembra che ci siano obiettivi contraddittori per GetHashCode quando applicati a oggetti mutabili, ovvero:

  1. Se a.Equals(b) allora a.GetHashCode() dovrebbe == b.GetHashCode() .
  2. Il valore di a.GetHashCode() non dovrebbe mai cambiare per la durata di a .

Questi sembrano contraddire naturalmente quando un object mutevole, perché se lo stato dell’object cambia, ci aspettiamo che il valore di .Equals() cambi, il che significa che GetHashCode dovrebbe cambiare per corrispondere al cambiamento in .Equals() , ma GetHashCode non dovrebbe modificare.

Perché sembra che ci sia questa contraddizione? Queste raccomandazioni non intendono essere applicate agli oggetti mutabili? Probabilmente supposto, ma potrebbe valere la pena menzionare che mi riferisco a classi non strutturate.

Risoluzione:

Sto marcando JaredPar come accettato, ma principalmente per l’interazione con i commenti. Per riassumere ciò che ho imparato da questo è che l’unico modo per raggiungere tutti gli obiettivi e per evitare un comportamento bizzarro nei casi limite è di ignorare solo Equals e GetHashCode basati su campi immutabili o implementare IEquatable . Questo tipo di sembra diminuire l’utilità degli Equals sovrascriventi per i tipi di riferimento, poiché da ciò che ho visto la maggior parte dei tipi di riferimento di solito non ha campi immutabili a meno che non siano memorizzati in un database relazionale per identificarli con le loro chiavi primarie.

Come funziona se i campi su cui si basa sono mutabili?

Non è nel senso che il codice hash cambierà quando l’object cambia. Questo è un problema per tutti i motivi elencati negli articoli che leggi. Sfortunatamente questo è il tipo di problema che in genere compare solo nei casi d’angolo. Quindi gli sviluppatori tendono a farla franca con il cattivo comportamento.

Inoltre, cosa succede se voglio che le ricerche nel dizionario, ecc., Siano basate sull’uguaglianza di riferimento e non sui miei equivalenti sovrascritti?

Finché si implementa un’interfaccia come IEquatable questo non dovrebbe essere un problema. La maggior parte delle implementazioni del dizionario sceglierà un comparatore di uguaglianza in un modo che utilizzi IEquatable IE su Object.ReferenceEquals. Anche senza IEquatable , la maggior parte di default chiamerà Object.Equals () che andrà poi nella tua implementazione.

Fondamentalmente nella maggior parte del codice in esecuzione voglio uguaglianza di riferimento e uso sempre == e non lo sovrascrivo.

Se ti aspetti che i tuoi oggetti si comportino con l’uguaglianza dei valori, devi sovrascrivere == e! = Per applicare l’uguaglianza dei valori per tutti i confronti. Gli utenti possono ancora utilizzare Object.ReferenceEquals se vogliono effettivamente l’uguaglianza di riferimento.

Ero solito presumere che il framework utilizzasse sempre == e non uguale a confrontare le cose

Ciò che il BCL usa è cambiato un po ‘nel tempo. Ora la maggior parte dei casi che usano l’uguaglianza prenderà un’istanza di IEqualityComparer e la userà per l’uguaglianza. Nei casi in cui uno non è specificato useranno EqualityComparer.Default per trovarne uno. Nel peggiore dei casi, per impostazione predefinita verrà chiamato Object.Equals

Se si dispone di un object mutabile, non è molto importante sovrascrivere il metodo GetHashCode, in quanto non è ansible utilizzarlo. Viene utilizzato ad esempio dalle raccolte Dictionary e HashSet per posizionare ciascun elemento in un bucket. Se si modifica l’object mentre è utilizzato come chiave nella raccolta, il codice hash non corrisponde più al bucket in cui si trova l’object, quindi la raccolta non funziona correttamente e non si può trovare nuovamente l’object.

Se si desidera che la ricerca non utilizzi il metodo GetHashCode o Equals della class, è sempre ansible fornire la propria implementazione di IEqualityComparer da utilizzare quando si crea il Dictionary .

Il metodo Equals è inteso per l’uguaglianza dei valori, quindi non è sbagliato implementarlo in questo modo.

Wow, in realtà sono diverse domande in uno :-). Quindi uno dopo l’altro:

è stato citato che il valore di GetHashCode non dovrebbe mai cambiare durante la vita dell’object. Come funziona se i campi su cui si basa sono mutabili?

Questo consiglio comune è inteso per il caso in cui si desidera utilizzare il proprio object come chiave in un HashTable / dizionario ecc. HashTables di solito richiedono che l’hash non cambi, perché lo usano per decidere come archiviare e recuperare la chiave. Se l’hash cambia, probabilmente HashTable non troverà più il tuo object.

Per citare i documenti dell’interfaccia Mappa di Java:

Nota: è necessario prestare molta attenzione se si utilizzano oggetti mutabili come chiavi della mappa. Il comportamento di una mappa non viene specificato se il valore di un object viene modificato in un modo che influisce su confronti uguali mentre l’object è una chiave nella mappa.

In generale è una ctriggers idea utilizzare qualsiasi tipo di object mutabile come chiave in una tabella hash: non è nemmeno chiaro cosa dovrebbe accadere se una chiave cambia dopo essere stata aggiunta alla tabella hash. La tabella hash deve restituire l’object memorizzato tramite la vecchia chiave, o tramite la nuova chiave, o tramite entrambi?

Quindi il vero consiglio è: usa solo oggetti immutabili come chiavi, e assicurati che il loro codice hash non cambi mai (di solito è automatico se l’object è immutabile).

Inoltre, cosa succede se voglio che le ricerche nel dizionario, ecc., Siano basate sull’uguaglianza di riferimento e non sui miei equivalenti sovrascritti?

Bene, trova un’implementazione di dizionario che funzioni in questo modo. Ma i dizionari della libreria standard usano l’hashcode & Equals, e non c’è modo di cambiarlo.

Sto principalmente sovrascrivendo Equals per la facilità di testare il mio codice di serializzazione che presumo di serializzare e deserializzare (in XML nel mio caso) uccide l’uguaglianza di riferimento, quindi voglio essere sicuro che sia corretto in base all’uguaglianza di valore. In questo caso, questa ctriggers pratica ha la precedenza su Equals?

No, lo troverei perfettamente accettabile. Tuttavia, non dovresti usare tali oggetti come chiavi in ​​un dizionario / hashtable, dato che sono mutabili. Vedi sopra.

L’argomento sottostante qui è come identificare gli oggetti in modo univoco. Lei menziona la serializzazione / deserializzazione che è importante perché l’integrità referenziale viene persa in quel processo.

La risposta breve, è che gli oggetti dovrebbero essere identificati in modo univoco dal più piccolo insieme di campi immutabili che possono essere utilizzati per farlo. Questi sono i campi da utilizzare quando si esegue l’override di GetHashCode ed Equals.

Per testare è perfettamente ragionevole definire qualunque asserzione tu abbia bisogno, di solito questi non sono definiti sul tipo stesso ma piuttosto come metodi di utilità nella suite di test. Forse un TestSuite.AssertEquals (MyClass, MyClass)?

Nota che GetHashCode ed Equals dovrebbero funzionare insieme. GetHashCode dovrebbe restituire lo stesso valore per due oggetti se sono uguali. Equals dovrebbe restituire true se e solo se due oggetti hanno lo stesso codice hash. (Si noti che è ansible che due oggetti non siano uguali ma potrebbero restituire lo stesso codice hash). Ci sono un sacco di pagine web che affrontano questo argomento frontalmente, ma solo google.

Non so di C #, essendo un noob relativo ad esso, ma in Java, se si esegue l’override di equals () è necessario anche eseguire l’override di hashCode () per mantenere il contratto tra loro (e viceversa) … E java ha anche la stessa cattura 22; fondamentalmente costringendo a usare campi immutabili … Ma questo è un problema solo per le classi che sono usate come una chiave hash, e Java ha implementazioni alternative per tutte le raccolte basate su hash … che forse non sono così veloci, ma lo fanno in modo esatto ti permettono di usare un object mutevole come chiave … è solo (di solito) accigliato come un “design scadente”.

E sento l’impulso di far notare che questo problema fondamentale è senza tempo … È stato in giro da quando Adam era un ragazzo.

Ho lavorato sul codice di Fortran che è più vecchio di me (sono 36) che si interrompe quando viene modificato un nome utente (come quando una ragazza si sposa o divorzia 😉 … Quindi è l’ingegneria, La soluzione adottata era : Il “metodo” GetHashCode ricorda l’hashCode calcolato in precedenza, ricalcola l’hashCode (cioè un indicatore isDirty virtuale) e se i campi chiave sono stati modificati restituisce null. Ciò fa sì che la cache elimini l’utente “sporco” (chiamando un altro GetPreviousHashCode) e quindi la cache restituisce null, causando la rilettura da parte dell’utente dal database. Un hack interessante e utile; anche se lo dico anch’io 😉

Cambierò la mutabilità (auspicabile solo nei casi d’angolo) per l’accesso O (1) (desiderabile in tutti i casi). Benvenuto in ingegneria; la terra del compromesso informato.

Saluti. Keith.