c# Perché è importante sovrascrivere GetHashCode quando il metodo Equals viene sovrascritto?




gethashcode implementation c# (10)

Solo per aggiungere le risposte sopra:

Se non si esegue l'override di Equals, il comportamento predefinito è che i riferimenti degli oggetti vengono confrontati. Lo stesso vale per l'hashcode: l'impianto predefinito si basa in genere su un indirizzo di memoria del riferimento. Poiché hai eseguito l'override di Equals, significa che il comportamento corretto è quello di confrontare qualsiasi cosa tu abbia implementato su Equals e non i riferimenti, quindi dovresti fare lo stesso per l'hashcode.

I client della tua classe si aspettano che l'hashcode abbia una logica simile al metodo equals, per esempio i metodi linq che usano un IEqualityComparer per prima cosa confrontano gli hashcode e solo se sono uguali essi confronteranno il metodo Equals () che potrebbe essere più costoso per eseguire, se non abbiamo implementato hashcode, l'oggetto uguale avrà probabilmente hashcode diversi (perché hanno un indirizzo di memoria diverso) e verrà determinato erroneamente come non uguale (Equals () non verrà nemmeno colpito).

Inoltre, ad eccezione del problema che potresti non essere in grado di trovare il tuo oggetto se lo hai usato in un dizionario (perché è stato inserito da un hashcode e quando lo cerchi, l'hashcode predefinito sarà probabilmente diverso e ancora Equals () non sarà nemmeno chiamato, come Marc Gravell spiega nella sua risposta, si introduce anche una violazione del dizionario o del concetto di hashset che non dovrebbe consentire chiavi identiche - hai già dichiarato che quegli oggetti sono essenzialmente gli stessi quando si esegue l'override di Equals così si li vogliamo entrambi come chiavi diverse su una struttura dati che suppongono di avere una chiave univoca, ma poiché hanno un diverso codice hash, la "stessa" chiave verrà inserita come una diversa.

Data la seguente classe

public class Foo
{
    public int FooId { get; set; }
    public string FooName { get; set; }

    public override bool Equals(object obj)
    {
        Foo fooItem = obj as Foo;

        return fooItem.FooId == this.FooId;
    }

    public override int GetHashCode()
    {
        // Which is preferred?

        return base.GetHashCode();

        //return this.FooId.GetHashCode();
    }
}

Ho sovrascritto il metodo Equals perché Foo rappresenta una riga per la tabella di Foo . Qual è il metodo preferito per sovrascrivere il GetHashCode ?

Perché è importante sovrascrivere GetHashCode ?


In realtà è molto difficile implementare correttamente GetHashCode() perché, oltre alle regole che Marc ha già menzionato, il codice hash non dovrebbe cambiare durante la vita di un oggetto. Pertanto i campi che vengono utilizzati per calcolare il codice hash devono essere immutabili.

Alla fine ho trovato una soluzione a questo problema quando lavoravo con NHibernate. Il mio approccio è quello di calcolare il codice hash dall'ID dell'oggetto. L'ID può essere impostato solo con il costruttore, quindi se vuoi cambiare l'ID, il che è molto improbabile, devi creare un nuovo oggetto che ha un nuovo ID e quindi un nuovo codice hash. Questo approccio funziona meglio con GUID perché è possibile fornire un costruttore senza parametri che genera in modo casuale un ID.


Che ne dite di:

public override int GetHashCode()
{
    return string.Format("{0}_{1}_{2}", prop1, prop2, prop3).GetHashCode();
}

Supponendo che le prestazioni non siano un problema :)


Abbiamo due problemi da affrontare.

  1. Non è possibile fornire un GetHashCode() ragionevole se è possibile modificare qualsiasi campo nell'oggetto. Inoltre, spesso un oggetto non verrà MAI utilizzato in una raccolta che dipende da GetHashCode() . Quindi il costo di implementare GetHashCode() spesso non vale la pena, o non è possibile.

  2. Se qualcuno mette il tuo oggetto in una collezione che chiama GetHashCode() e hai Equals() override di Equals() senza fare in modo che GetHashCode() comporti in modo corretto, quella persona potrebbe passare giorni a rintracciare il problema.

Pertanto per impostazione predefinita lo faccio.

public class Foo
{
    public int FooId { get; set; }
    public string FooName { get; set; }

    public override bool Equals(object obj)
    {
        Foo fooItem = obj as Foo;

        return fooItem.FooId == this.FooId;
    }

    public override int GetHashCode()
    {
        // Some comment to explain if there is a real problem with providing GetHashCode() 
        // or if I just don't see a need for it for the given class
        throw new Exception("Sorry I don't know what GetHashCode should do for this class");
    }
}

Eseguendo l'override di Equals, stai fondamentalmente affermando che sei quello che sa meglio come confrontare due istanze di un determinato tipo, quindi è probabile che tu sia il miglior candidato per fornire il miglior codice hash.

Questo è un esempio di come ReSharper scrive una funzione GetHashCode () per te:

public override int GetHashCode()
{
    unchecked
    {
        var result = 0;
        result = (result * 397) ^ m_someVar1;
        result = (result * 397) ^ m_someVar2;
        result = (result * 397) ^ m_someVar3;
        result = (result * 397) ^ m_someVar4;
        return result;
    }
}

Come puoi vedere, prova a indovinare un buon codice di hash basato su tutti i campi della classe, ma dal momento che conosci il dominio o gli intervalli di valori del tuo oggetto, potresti comunque fornirne uno migliore.


Non è necessariamente importante; dipende dalla dimensione delle collezioni e dai requisiti di rendimento e dal fatto che la classe verrà utilizzata in una libreria in cui potresti non conoscere i requisiti di rendimento. So spesso che le dimensioni della mia collezione non sono molto grandi e il mio tempo è più prezioso di qualche microsecondo di prestazioni ottenute creando un codice hash perfetto; quindi (per eliminare il fastidioso avviso del compilatore) uso semplicemente:

   public override int GetHashCode()
   {
      return base.GetHashCode();
   }

(Naturalmente potrei usare un #pragma per disattivare anche l'avviso, ma preferisco in questo modo).

Ovviamente, quando si è nella posizione in cui si ha bisogno della performance, si applicano tutti i problemi menzionati da altri. La cosa più importante - altrimenti otterrai risultati errati quando recuperi elementi da un set di hash o da un dizionario: il codice hash non deve variare con il tempo di vita di un oggetto (più precisamente, durante il tempo ogni volta che è necessario il codice hash, ad esempio una chiave in un dizionario): ad esempio, il seguente è errato poiché Value è pubblico e quindi può essere modificato esternamente alla classe durante la vita dell'istanza, quindi non è necessario utilizzarlo come base per il codice hash:


   class A
   {
      public int Value;

      public override int GetHashCode()
      {
         return Value.GetHashCode(); //WRONG! Value is not constant during the instance's life time
      }
   }    

D'altra parte, se Value non può essere cambiato, è ok usare:


   class A
   {
      public readonly int Value;

      public override int GetHashCode()
      {
         return Value.GetHashCode(); //OK  Value is read-only and can't be changed during the instance's life time
      }
   }


Sotto usando la riflessione mi sembra un'opzione migliore considerando le proprietà pubbliche come con questo non devi preoccuparti di aggiunta / rimozione di proprietà (anche se non così scenario comune). Questo mi è sembrato che stia migliorando anche. (Tempo comparato usando il cronometro Diagonistics).

    public int getHashCode()
    {
        PropertyInfo[] theProperties = this.GetType().GetProperties();
        int hash = 31;
        foreach (PropertyInfo info in theProperties)
        {
            if (info != null)
            {
                var value = info.GetValue(this,null);
                if(value != null)
                unchecked
                {
                    hash = 29 * hash ^ value.GetHashCode();
                }
            }
        }
        return hash;  
    }

È a mia conoscenza che l'originale GetHashCode () restituisce l'indirizzo di memoria dell'oggetto, quindi è essenziale sovrascriverlo se si desidera confrontare due oggetti diversi.

EDITED: non era corretto, il metodo originale GetHashCode () non può assicurare l'uguaglianza di 2 valori. Sebbene gli oggetti uguali restituiscano lo stesso codice hash.


È perché il framework richiede che due oggetti uguali debbano avere lo stesso hashcode. Se si esegue l'override del metodo equals per eseguire un confronto speciale di due oggetti e i due oggetti sono considerati uguali dal metodo, anche il codice hash dei due oggetti deve essere uguale. (Dizionari e Hashtables si basano su questo principio).


Si prega di non dimenticare di controllare il parametro obj su null quando Equals() override di Equals() . E anche confrontare il tipo.

public override bool Equals(object obj)
{
    if (obj == null || GetType() != obj.GetType())
        return false;

    Foo fooItem = obj as Foo;

    return fooItem.FooId == this.FooId;
}

Il motivo è: Equals deve restituire false in confronto a null . Vedi anche http://msdn.microsoft.com/en-us/library/bsc2ak47.aspx





hashcode