Dal momento che non sono riuscito a trovare una risposta che spieghi perché dovremmo eseguire l'override GetHashCode
e Equals
per le strutture personalizzate e perché l'implementazione predefinita "non è probabile che sia adatta per l'uso come chiave in una tabella hash", lascerò un link a questo blog post , che spiega perché con un esempio reale di un problema che si è verificato.
Consiglio di leggere l'intero post, ma ecco un riassunto (enfasi e chiarimenti aggiunti).
Motivo per cui l'hash predefinito per le strutture è lento e non molto buono:
Nel modo in cui è progettato il CLR, ogni chiamata a un membro definito in System.ValueType
o System.Enum
tipi [può] causare un'allocazione di boxe [...]
Un implementatore di una funzione hash deve affrontare un dilemma: fare una buona distribuzione della funzione hash o velocizzarla. In alcuni casi, è possibile ottenere tutti e due, ma è difficile fare questo modo generico in ValueType.GetHashCode
.
La funzione hash canonica di una struttura "combina" codici hash di tutti i campi. Ma l'unico modo per ottenere un codice hash di un campo in un ValueType
metodo è usare la riflessione . Quindi, gli autori del CLR hanno deciso di scambiare velocità sulla distribuzione e la GetHashCode
versione predefinita restituisce solo un codice hash di un primo campo non nullo e lo "munge" con un ID di tipo [...] Questo è un comportamento ragionevole a meno che non lo sia . Ad esempio, se sei abbastanza sfortunato e il primo campo della tua struttura ha lo stesso valore per la maggior parte delle istanze, una funzione hash fornirà lo stesso risultato in ogni momento. E, come puoi immaginare, questo causerà un drastico impatto sulle prestazioni se queste istanze sono archiviate in un set di hash o in una tabella di hash.
[...] L' implementazione basata sulla riflessione è lenta . Molto lento.
[...] Entrambi ValueType.Equals
e ValueType.GetHashCode
hanno un'ottimizzazione speciale. Se un tipo non ha "puntatori" ed è impacchettato [...] correttamente, vengono utilizzate versioni più ottimali: GetHashCode
scorre su un'istanza e blocchi XOR di 4 byte e il Equals
metodo confronta due istanze usando memcmp
. [...] Ma l'ottimizzazione è molto complicata. Innanzitutto, è difficile sapere quando l'ottimizzazione è abilitata [...] In secondo luogo, un confronto della memoria non fornirà necessariamente i risultati giusti . Ecco un semplice esempio: [...] -0.0
e +0.0
sono uguali ma hanno diverse rappresentazioni binarie.
Problema del mondo reale descritto nel post:
private readonly HashSet<(ErrorLocation, int)> _locationsWithHitCount;
readonly struct ErrorLocation
{
// Empty almost all the time
public string OptionalDescription { get; }
public string Path { get; }
public int Position { get; }
}
Abbiamo usato una tupla che conteneva una struttura personalizzata con l'implementazione di uguaglianza predefinita. E sfortunatamente, la struttura aveva un primo campo opzionale che era quasi sempre uguale a [stringa vuota] . Le prestazioni sono state OK fino a quando il numero di elementi nel set è aumentato in modo significativo causando un vero problema di prestazioni, impiegando pochi minuti per inizializzare una raccolta con decine di migliaia di articoli.
Quindi, per rispondere alla domanda "in quali casi dovrei impacchettare i miei e in quali casi posso fare affidamento in modo sicuro sull'implementazione predefinita", almeno nel caso delle strutture , dovresti sovrascrivere Equals
e GetHashCode
ogni volta che la tua struttura personalizzata potrebbe essere utilizzata come digitare una tabella hash o Dictionary
.
Vorrei anche raccomandare l'implementazione IEquatable<T>
in questo caso, per evitare il pugilato.
Come hanno detto le altre risposte, se stai scrivendo una classe , l'hash predefinito usando l'uguaglianza di riferimento di solito va bene, quindi in questo caso non mi preoccuperei, a meno che tu non abbia bisogno di scavalcare Equals
(allora dovresti scavalcare di GetHashCode
conseguenza).