Implementazione predefinita per Object.GetHashCode ()


162

Come funziona l'implementazione predefinita per GetHashCode()? E gestisce strutture, classi, array, ecc. In modo efficiente e abbastanza buono?

Sto cercando di decidere in quali casi devo fare i miei e in quali casi posso fare affidamento sull'implementazione predefinita per fare bene. Non voglio reinventare la ruota, se possibile.


Dai un'occhiata al commento che ho lasciato sull'articolo: stackoverflow.com/questions/763731/gethashcode-extension-method
Paul Westcott,


34
A parte: è possibile ottenere l'hashcode predefinito (anche quando GetHashCode()è stato sovrascritto) utilizzandoSystem.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(obj)
Marc Gravell

@MarcGravell grazie per aver contribuito, stavo cercando esattamente questa risposta.
Andrew Savinykh,

@MarcGravell Ma come lo farei con un altro metodo?
Tomáš Zato - Ripristina Monica

Risposte:


86
namespace System {
    public class Object {
        [MethodImpl(MethodImplOptions.InternalCall)]
        internal static extern int InternalGetHashCode(object obj);

        public virtual int GetHashCode() {
            return InternalGetHashCode(this);
        }
    }
}

InternalGetHashCode è associato a una funzione ObjectNative :: GetHashCode nel CLR, che assomiglia a questo:

FCIMPL1(INT32, ObjectNative::GetHashCode, Object* obj) {  
    CONTRACTL  
    {  
        THROWS;  
        DISABLED(GC_NOTRIGGER);  
        INJECT_FAULT(FCThrow(kOutOfMemoryException););  
        MODE_COOPERATIVE;  
        SO_TOLERANT;  
    }  
    CONTRACTL_END;  

    VALIDATEOBJECTREF(obj);  

    DWORD idx = 0;  

    if (obj == 0)  
        return 0;  

    OBJECTREF objRef(obj);  

    HELPER_METHOD_FRAME_BEGIN_RET_1(objRef);        // Set up a frame  

    idx = GetHashCodeEx(OBJECTREFToObject(objRef));  

    HELPER_METHOD_FRAME_END();  

    return idx;  
}  
FCIMPLEND

L'implementazione completa di GetHashCodeEx è piuttosto ampia, quindi è più semplice collegarsi al codice sorgente C ++ .


5
Tale citazione della documentazione deve provenire da una versione molto antica. Non è più scritto così negli attuali articoli di MSDN, probabilmente perché è del tutto sbagliato.
Hans Passant,

4
Hanno cambiato la formulazione, sì, ma in sostanza dice sempre la stessa cosa: "Di conseguenza, l'implementazione predefinita di questo metodo non deve essere utilizzata come identificatore univoco di oggetti per scopi di hashing".
David Brown,

7
Perché la documentazione afferma che l'implementazione non è particolarmente utile per l'hashing? Se un oggetto è uguale a se stesso e nient'altro, qualsiasi metodo di codice hash che restituirà sempre lo stesso valore per una determinata istanza di oggetto e generalmente restituirà valori diversi per istanze diverse, qual è il problema?
supercat

3
@ ta.speot.is: se quello che vuoi è determinare se una particolare istanza è già stata aggiunta in un dizionario, l'uguaglianza di riferimento è perfetta. Con le stringhe, come noterai, di solito sei più interessato a sapere se è già stata aggiunta una stringa contenente la stessa sequenza di caratteri . Ecco perché stringsostituisce GetHashCode. D'altra parte, supponiamo che tu voglia tenere un conto di quante volte vari controlli elaborano gli Painteventi. È possibile utilizzare un Dictionary<Object, int[]>(ogni int[]elemento memorizzato conterrebbe esattamente un elemento).
supercat

6
@ It'sNotALie. Quindi ringrazia Archive.org per averne una copia ;-)
RobIII

88

Per una classe, i valori predefiniti sono essenzialmente uguaglianza di riferimento, e di solito va bene. Se si scrive una struttura, è più comune ignorare l'uguaglianza (non ultimo per evitare la boxe), ma è molto raro scrivere una struttura comunque!

Quando si ignora l'uguaglianza, si dovrebbe sempre avere una corrispondenza Equals()e GetHashCode()(cioè per due valori, seEquals() restituisce true devono restituire lo stesso codice hash, ma non è necessario il contrario ) - ed è comune fornire anche ==/ !=operatori, e spesso a strumentoIEquatable<T> anche.

Per generare il codice hash, è comune utilizzare una somma fattorizzata, poiché ciò evita le collisioni sui valori accoppiati, ad esempio per un hash di campo 2 di base:

unchecked // disable overflow, for the unlikely possibility that you
{         // are compiling with overflow-checking enabled
    int hash = 27;
    hash = (13 * hash) + field1.GetHashCode();
    hash = (13 * hash) + field2.GetHashCode();
    return hash;
}

Questo ha il vantaggio che:

  • l'hash di {1,2} non è uguale all'hash di {2,1}
  • l'hash di {1,1} non è lo stesso dell'hash di {2,2}

ecc. - che può essere comune solo usando una somma non ponderata, o xor ( ^), ecc.


Punto eccellente sul vantaggio di un algoritmo di somma fattorizzata; qualcosa che non avevo realizzato prima!
Scappatoia

La somma fattorizzata (come scritto sopra) non causa occasionalmente eccezioni di overflow?
sinelaw,

4
@sinelaw sì, dovrebbe essere eseguito unchecked. Fortunatamente, uncheckedè l'impostazione predefinita in C #, ma sarebbe meglio renderlo esplicito; a cura di
Marc Gravell

7

La documentazione per il GetHashCodemetodo per Object afferma che "l'implementazione predefinita di questo metodo non deve essere utilizzata come identificatore di oggetto univoco per scopi di hashing". e quello per ValueType dice "Se si chiama il metodo GetHashCode del tipo derivato, è probabile che il valore restituito non sia adatto all'uso come chiave in una tabella hash.".

I tipi di dati di base come byte, short, int, long, chare stringimplementare un metodo GetHashCode buona. Alcune altre classi e strutture, come Pointad esempio, implementano aGetHashCode metodo che può o non può essere adatto alle tue esigenze specifiche. Devi solo provarlo per vedere se è abbastanza buono.

La documentazione per ogni classe o struttura può dirti se sovrascrive l'implementazione predefinita o meno. In caso contrario, è necessario utilizzare la propria implementazione. Per qualsiasi classe o struttura creata dall'utente nel punto in cui è necessario utilizzare il GetHashCodemetodo, è necessario effettuare la propria implementazione che utilizza i membri appropriati per calcolare il codice hash.


2
Non sarei d'accordo sul fatto che dovresti aggiungere regolarmente la tua implementazione. Semplicemente, la stragrande maggioranza delle classi (in particolare) non sarà mai testata per l'uguaglianza - o dove si trovano, l'uguaglianza di riferimento integrata va bene. Nella (già rara) occasione di scrivere una struttura, sarebbe più comune, vero.
Marc Gravell

@Marc Gravel: ovviamente non è quello che intendevo dire. Aggiusterò l'ultimo paragrafo. :)
Guffa,

I tipi di dati di base non implementano un buon metodo GetHashCode, almeno nel mio caso. Ad esempio, GetHashCode per int restituisce il numero stesso: (123) .GetHashCode () restituisce 123.
fdermishin

5
@ user502144 E cosa c'è che non va? È un identificatore univoco perfetto che è facile da calcolare, senza falsi positivi sull'uguaglianza ...
Richard Rast

@Richard Rast: va bene, tranne che le chiavi possono essere mal distribuite quando usate in un Hashtable. Date un'occhiata a questa risposta: stackoverflow.com/a/1388329/502144
fdermishin

5

Dal momento che non sono riuscito a trovare una risposta che spieghi perché dovremmo eseguire l'override GetHashCodee Equalsper 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.ValueTypeo System.Enumtipi [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 ValueTypemetodo è usare la riflessione . Quindi, gli autori del CLR hanno deciso di scambiare velocità sulla distribuzione e la GetHashCodeversione 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.Equalse ValueType.GetHashCodehanno un'ottimizzazione speciale. Se un tipo non ha "puntatori" ed è impacchettato [...] correttamente, vengono utilizzate versioni più ottimali: GetHashCodescorre su un'istanza e blocchi XOR di 4 byte e il Equalsmetodo 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.0e +0.0sono 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 Equalse GetHashCodeogni 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 GetHashCodeconseguenza).


1

In generale, se si sostituisce Equals, si desidera sovrascrivere GetHashCode. Il motivo è perché entrambi sono usati per confrontare l'uguaglianza della tua classe / struttura.

Equals viene usato quando si controlla Foo A, B;

if (A == B)

Poiché sappiamo che è probabile che il puntatore non corrisponda, possiamo confrontare i membri interni.

Equals(obj o)
{
    if (o == null) return false;
    MyType Foo = o as MyType;
    if (Foo == null) return false;
    if (Foo.Prop1 != this.Prop1) return false;

    return Foo.Prop2 == this.Prop2;
}

GetHashCode è generalmente utilizzato dalle tabelle hash. L'hashcode generato dalla tua classe dovrebbe essere sempre lo stesso per uno stato che dà le classi.

Di solito lo faccio,

GetHashCode()
{
    int HashCode = this.GetType().ToString().GetHashCode();
    HashCode ^= this.Prop1.GetHashCode();
    etc.

    return HashCode;
}

Alcuni diranno che l'hashcode dovrebbe essere calcolato solo una volta per durata dell'oggetto, ma non sono d'accordo (e probabilmente sbaglio).

Utilizzando l'implementazione predefinita fornita dall'oggetto, a meno che tu non abbia lo stesso riferimento a una delle tue classi, non saranno uguali tra loro. Sostituendo Equals e GetHashCode, è possibile segnalare l'uguaglianza in base a valori interni anziché al riferimento agli oggetti.


2
L'approccio ^ = non è un approccio particolarmente valido per generare un hash - tende a provocare molte collisioni comuni / prevedibili - ad esempio se Prop1 = Prop2 = 3.
Marc Gravell

Se i valori sono gli stessi, non vedo alcun problema con la collisione poiché gli oggetti sono uguali. Il 13 * Hash + NewHash sembra comunque interessante.
Bennett Dill,

2
Ben: provalo per Obj1 {Prop1 = 12, Prop2 = 12} e Obj2 {Prop1 = 13, Prop2 = 13}
Tomáš Kafka

0

Se hai solo a che fare con i POCO, puoi usare questa utility per semplificarti un po 'la vita:

var hash = HashCodeUtil.GetHashCode(
           poco.Field1,
           poco.Field2,
           ...,
           poco.FieldN);

...

public static class HashCodeUtil
{
    public static int GetHashCode(params object[] objects)
    {
        int hash = 13;

        foreach (var obj in objects)
        {
            hash = (hash * 7) + (!ReferenceEquals(null, obj) ? obj.GetHashCode() : 0);
        }

        return hash;
    }
}
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.