Il metodo GetHashCode

Ancora oggi noto come il metodo GetHashCode risulti un mistero per molte persone (nascerebbe la domanda: Ma se non lo conosci perchè lo implementi?).
Proprio ieri mi è capitato di leggere un articolo in rete e trovare al suo interno un'implementazione del metodo GetHashCode, come il seguente:

public class Book
{
    public Book()
    {
        BookID = -1;
        BookTitle = string.Empty;
    }


    public int BookID { get; set; }
    public string BookTitle { get; set; }


    public override string ToString()
    {
        return BookTitle ?? string.Empty;
    }


    public override bool Equals(object obj)
    {
        var book = obj as Book;
        if (book != null)
            return (BookID == book.BookID);
        return false;
    }


    public override int GetHashCode()
    {
        return string.Format("{0}{1}", BookID, BookTitle).GetHashCode();
    }
}

Come dice il buon Jeffrey Richter (padre del CRL) in "CLR via C#", l’HashCode è rappresentativo dell'oggetto e deve essere consistente con la relazione di uguaglianza.

Andando a ridefinire quest’ultima tramite il metodo Equals è necessario fare lo stesso anche per il metodo GetHashCode, e di fatto il compilatore solleva un Warning se ciò non avviene.
Questo serve a garantire che due oggetti definiti uguali abbiano lo stesso HashCode, altrimenti alcuni classi (come l'Hashtable, HashSet<T>, Dictionary<T,K>, ecc) potrebbero non funzionare.

L'implementazione del metodo GetHashCode deve rispettera tre criteri:

- Se due oggetti dello stesso tipo rappresentano lo stesso valore, la funzione hash deve restituire lo stesso valore di costante per entrambi gli oggetti.

- Per ottenere le migliori prestazioni, una funzione hash deve generare una distribuzione casuale per tutti gli input.

- La funzione hash deve restituire esattamente lo stesso valore indipendentemente da qualsiasi modifica apportata all'oggetto.

Partendo da quest'ultimo punto, si può capire che l'implementazione andrebbe fatta in un ValueType o, al massimo, in una classe in cui i field utilizzati per il calcolo dell'Hash siano readonly e quindi immutabili.

Lo snippet seguente mostra come due classi aventi valori differenti risultino uguali al metodo Equals ma differenti al metodo GetHashCode, a causa dell'errata implementazione mostrata sopra:

var b1 = new Book();
b1.BookID = 10;
b1.BookTitle = "Titolo1";


var b2 = new Book();
b2.BookID = 10;
b2.BookTitle = "Titolo2";


int hc1 = b1.GetHashCode();
int hc2 = b2.GetHashCode();


Console.WriteLine(hc1 == hc2); //ritorna false
Console.WriteLine(b1.Equals(b2)); //ritorna true

Anche se molti esempi in rete riportano l'utilizzo dell'operatore XOR (eXclusive OR) per il calcolo dell'HashCode della classe, io consiglio sempre l'implementazione mostrata da Marco qui, in quanto se si hanno due fields dello stesso tipo e si inverte il loro valore, l'HashCode restituito è sempre lo stesso, mentre non dovrebbe esserlo.
Ovviamente si potrebbe aggiungere che nel .NET Framework moltissime classi (Hashtable, Dictionary, ecc) fanno uso massiccio di questo metodo e ne consegue che invocare uno string.format ad ogni richiesta non è ottimale per l'applicaizone.

Per concludere, fate attenzione e soprattutto interrogatevi sulla reale necessità di reimplementare Equals e GetHashCode.


Ciauz


Comments