Qual è il ruolo di GetHashCode in IEqualityComparer <T> in .NET?


142

Sto cercando di capire il ruolo del metodo GetHashCode dell'interfaccia IEqualityComparer.

Il seguente esempio è tratto da MSDN:

using System;
using System.Collections.Generic;
class Example {
    static void Main() {
        try {

            BoxEqualityComparer boxEqC = new BoxEqualityComparer();

            Dictionary<Box, String> boxes = new Dictionary<Box,
                                                string>(boxEqC);

            Box redBox = new Box(4, 3, 4);
            Box blueBox = new Box(4, 3, 4);

            boxes.Add(redBox, "red");
            boxes.Add(blueBox, "blue");

            Console.WriteLine(redBox.GetHashCode());
            Console.WriteLine(blueBox.GetHashCode());
        }
        catch (ArgumentException argEx) {

            Console.WriteLine(argEx.Message);
        }
    }
}

public class Box {
    public Box(int h, int l, int w) {
        this.Height = h;
        this.Length = l;
        this.Width = w;
    }
    public int Height { get; set; }
    public int Length { get; set; }
    public int Width { get; set; }
}

class BoxEqualityComparer : IEqualityComparer<Box> {

    public bool Equals(Box b1, Box b2) {
        if (b1.Height == b2.Height & b1.Length == b2.Length
                            & b1.Width == b2.Width) {
            return true;
        }
        else {
            return false;
        }
    }

    public int GetHashCode(Box bx) {
        int hCode = bx.Height ^ bx.Length ^ bx.Width;
        return hCode.GetHashCode();
    }
}

L'implementazione del metodo Equals non dovrebbe essere sufficiente per confrontare due oggetti Box? È qui che diciamo al framework la regola utilizzata per confrontare gli oggetti. Perché è necessario GetHashCode?

Grazie.

Lucian


Leggi: en.wikipedia.org/wiki/Hash_table, quindi vedi se comprendi meglio lo scopo di GetHashCode.
spender

1
Guarda questa grande risposta: stackoverflow.com/a/3719802/136967
Mikhail

Risposte:


201

Prima un po 'di sfondo ...

Ogni oggetto in .NET ha un metodo Equals e un metodo GetHashCode.

Il metodo Equals viene utilizzato per confrontare un oggetto con un altro oggetto - per vedere se i due oggetti sono equivalenti.

Il metodo GetHashCode genera una rappresentazione intera a 32 bit dell'oggetto. Poiché non vi è limite alla quantità di informazioni che un oggetto può contenere, alcuni codici hash sono condivisi da più oggetti, quindi il codice hash non è necessariamente univoco.

Un dizionario è una struttura di dati davvero interessante che scambia un footprint di memoria più elevato in cambio di costi (più o meno) costanti per le operazioni Aggiungi / Rimuovi / Ottieni. È una cattiva scelta per iterare però. Internamente, un dizionario contiene una matrice di bucket, in cui è possibile memorizzare i valori. Quando si aggiungono una chiave e un valore a un dizionario, il metodo GetHashCode viene chiamato sulla chiave. L'hashcode restituito viene utilizzato per determinare l'indice del bucket in cui deve essere memorizzata la coppia chiave / valore.

Quando si desidera accedere al valore, si passa nuovamente la chiave. Il metodo GetHashCode viene chiamato sulla chiave e si trova il bucket contenente il valore.

Quando un IEqualityComparer viene passato nel costruttore di un dizionario, vengono utilizzati i metodi IEqualityComparer.Equals e IEqualityComparer.GetHashCode anziché i metodi sugli oggetti Key.

Ora per spiegare perché entrambi i metodi sono necessari, considera questo esempio:

BoxEqualityComparer boxEqC = new BoxEqualityComparer(); 

Dictionary<Box, String> boxes = new Dictionary<Box, string>(boxEqC); 

Box redBox = new Box(100, 100, 25);
Box blueBox = new Box(1000, 1000, 25);

boxes.Add(redBox, "red"); 
boxes.Add(blueBox, "blue"); 

Utilizzando il metodo BoxEqualityComparer.GetHashCode nell'esempio, entrambe queste caselle hanno lo stesso hashcode - 100 ^ 100 ^ 25 = 1000 ^ 1000 ^ 25 = 25 - anche se chiaramente non sono lo stesso oggetto. Il motivo per cui sono lo stesso hashcode in questo caso è perché si sta utilizzando l'operatore ^ (bitwise esclusivo-OR) in modo che 100 ^ 100 annulli lasciando zero, così come 1000 ^ 1000. Quando due oggetti diversi hanno la stessa chiave, la chiamiamo collisione.

Quando aggiungiamo due coppie chiave / valore con lo stesso codice hash a un dizionario, vengono entrambi archiviati nello stesso bucket. Quindi, quando vogliamo recuperare un valore, il metodo GetHashCode viene chiamato sulla nostra chiave per individuare il bucket. Poiché nel bucket è presente più di un valore, il dizionario esegue l'iterazione su tutte le coppie chiave / valore nel bucket richiamando il metodo Equals sulle chiavi per trovare quello corretto.

Nell'esempio che hai pubblicato, le due caselle sono equivalenti, quindi il metodo Equals restituisce true. In questo caso il dizionario ha due chiavi identiche, quindi genera un'eccezione.

TLDR

Quindi, in sintesi, il metodo GetHashCode viene utilizzato per generare un indirizzo in cui è archiviato l'oggetto. Quindi un dizionario non deve cercarlo. Calcola semplicemente l'hashcode e salta in quella posizione. Il metodo Equals è un test migliore dell'uguaglianza, ma non può essere utilizzato per mappare un oggetto in uno spazio degli indirizzi.


4
Per quelli che si chiedono cos'è l'operatore ^, questo è l'operatore OR bit a bit esclusivo, vedere msdn.microsoft.com/en-us/library/zkacc7k1.aspx .
R. Schreurs,

2
Giusto per evidenziarlo esplicitamente: ( msdn.microsoft.com/en-us/library/ms132155.aspx ) Note per gli implementatori Le implementazioni sono necessarie per garantire che se il metodo Equals restituisce true per due oggetti xey, il valore restituito dal metodo GetHashCode per x deve essere uguale al valore restituito per y.
Diego Frehner,

2
@DiegoFrehner - Hai perfettamente ragione. Un'altra cosa che può far inciampare le persone è che il valore del metodo GetHashCode non dovrebbe variare se l'oggetto viene modificato. Quindi i campi all'interno dell'oggetto da cui dipende GetHashCode dovrebbero essere di sola lettura (immutabili). C'è una spiegazione qui: stackoverflow.com/a/4868940/469701
sheikhjabootie

1
@Acentric: il codice hash di un oggetto non dovrebbe cambiare a meno che non sia mutato in modo tale da influire sull'uguaglianza. Se una classe può essere modificata in modo tale da influire sull'uguaglianza, il codice dovrebbe evitare di archiviare in un dizionario qualsiasi istanza che potrebbe essere esposta al codice che la muterà mentre si trova nel dizionario. Se il codice che memorizza l'oggetto rispetta tale regola, può essere utile disporre di un codice hash che rifletta lo stato mutabile. Peccato che .NET non distingua meglio l'uguaglianza e l'equivalenza degli stati, poiché entrambi sono concetti utili.
supercat

3
@Acentric: anche oltre all'utilizzo del codice hash per l'indirizzamento della tabella hash, l' idea fondamentale dietro un codice hash è che la conoscenza che due oggetti hanno codici hash diversi implica che sono disuguali e non è necessario confrontarli. Come corollario, la consapevolezza che i codici hash di molti oggetti non corrispondono al codice hash di un determinato oggetto implica che nessuno di essi è uguale all'oggetto. L'uso di un codice hash per l'indirizzamento è fondamentalmente un modo per ignorare gli oggetti che hanno codici hash diversi.
supercat

9

GetHashCode viene utilizzato nelle raccolte di dizionari e crea hash per la memorizzazione di oggetti al suo interno. Ecco un bell'articolo perché e come usare IEqualtyComparer e GetHashCode http://dotnetperls.com/iequalitycomparer


4
Altro: Se hai bisogno di confrontare Equals sarebbe enouf, ma quando hai bisogno di ottenere l'elemento dal Dizionario è più facile farlo con l'hash, non usando Equals .
Ash,

5

Mentre sarebbe possibile per un Dictionary<TKey,TValue>avere i suoi GetValuemetodi e simili chiamare Equalssu ogni singolo tasto memorizzato per vedere se corrisponde a quello che si sta cercando, sarebbe molto lento. Invece, come molte raccolte basate su hash, si basa GetHashCodesull'esclusione rapida dalla maggior parte dei valori non corrispondenti. Se chiamare GetHashCodeun oggetto da ricercare produce 42, e una collezione ha 53.917 articoli, ma chiamare GetHashCode53.914 degli articoli ha prodotto un valore diverso da 42, allora solo 3 articoli dovranno essere confrontati con quelli ricercati. Gli altri 53.914 possono essere tranquillamente ignorati.

Il motivo a GetHashCodeè incluso in an IEqualityComparer<T>è per consentire la possibilità che il consumatore di un dizionario possa voler considerare oggetti uguali che normalmente non si considererebbero uguali. L'esempio più comune sarebbe un chiamante che desidera utilizzare stringhe come chiavi ma utilizza confronti senza distinzione tra maiuscole e minuscole. Per farlo funzionare in modo efficiente, il dizionario dovrà avere una qualche forma di funzione hash che produrrà lo stesso valore per "Fox" e "FOX", ma si spera produca qualcos'altro per "box" o "zebra". Poiché il GetHashCodemetodo integrato Stringnon funziona in questo modo, il dizionario dovrà ottenere tale metodo da qualche altra parte,IEqualityComparer<T>Equals metodo che considera "Fox" e "FOX" identici tra loro, ma non a "box" o "zebra".


La risposta corretta e precisa alla domanda! GetHashCode () deve integrare Equals () per gli oggetti in questione.
Sumith,

@Sumith: molte discussioni sull'hashing parlano di secchi, ma penso che sia più utile pensare all'esclusione. Se i confronti sono costosi, l'hashing potrebbe offrire vantaggi anche quando si utilizzano raccolte che non sono organizzate in bucket.
Supercat,
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.