A che cosa serve GetHashCode?
Domanda che mi è sovvenuta perché dovendo predisporre per una serie di classi un Override dell'operatore == in modo da poterle comparare semplicemente, il sistema mi obbliga (per poter compilare) a fare l'override anche di due metodi della classe che sono:
Equals()
GetHashCode()
Se il primo è, almeno dal nome, semplice da capire il secondo no, pertanto sono andata a cercare di cosa si tratta e come è opportuno farne l'override per mantenerlo corretto. Questa è la traduzione della risposta che ho trovato più coerente:
A cosa serve GetHashCode?
Viene utilizzato per determinare se due oggetti sono identici. Viene utilizzato da Equals nella sua forma standard per determinare se due oggetti sono lo stesso oggetto. Questo è vero se tutti gli attributi della classe sono uguali.
Se si vuole creade le tue strutture o classi e compararle utilizzando Equals, devi costruire il tuo valore di Hash. Il modo più semplice per farlo è fare un or esclusivo tra i valori di Hash degli attributi che identificano il tuo oggetto e ritornare quel valore. Se si effettua l'Override di Equals è necessario fare l'override di GetHashCode().
Operatori di Uguaglianza, e disuguaglianza
Faccio seguito al post precedente, perché mi sono spinta un po' più a fondo sull'argomento e seguendo alcune indicazioni fornite dalle specifiche di base di .NET e dalle best practices che chi ne sa più di me ha fornito sono arrivata a qualcosa di un po' più articolato, ovvero, se volete generare una classe di tipo Entity (che rappresenta quindi un Dato di qualche genere) e volete che questo oggetto supporti la possibilità di essere inserito in una collection, di essere ricercato, ordinato, comparato in modo standard, è necessario procedere nel seguente modo:
- Implementare il vostro oggetto in forma base, quindi definirne i campi, le proprietà e la funzione base ToString (perché quella dell'object è un po' misera).
- Se volete rendere l'oggetto serializzabile, quindi usabile via WebServices oppure semplicemente scrivibile/leggibile su disco, generare le funzioni ReadXml e WriteXml
- Implementare l'interfaccia IComparable, che si compone di un'unico metodo CompareTo che ci permette di stabilire un metodo univoco per definire se due dei nostri oggetti sono uguali, maggiori, minori l'uno all'altro.
- Implementare l'operatore di Uguaglianza (==) e obbligatoriamente quello di disuguaglianza (!=) e di conseguenza su richiesta del compilatore implementare l'override dei metodi Equals e GetHashCode.
Una cosa a cui fare molta attenzione è la seguente, le specifiche base di .NET dicono che la funzione Equals deve ritornare true solo se 2 oggetti sono lo stesso oggetto, mentre l'operatore di uguaglianza (==)può coincidere con Equals oppure fornire una uguaglianza in base ai contenuti. E qui, voi, come me direte "Omioddio ma come è possibile..." è molto semplice, anche se un pochino contorto, facciamo un esempio:
DateTime DataUno = new DateTime( 2006, 05, 21);
DateTime DataDue = new DateTime( 2006, 05, 21);
bool compareValue = DataUno == DataDue;
bool equalValue = DataUno.Equals(DataDue);
In questo caso, compareValue è true, mentre equalValue è false
DateTime DataUno = new DateTime( 2006, 05, 21);
DateTime DataDue = DataUno
bool compareValue = DataUno == DataDue;
bool equalValue = DataUno.Equals(DataDue);
In questo caso, invece, entrambi i valori di comparazione sono true.
C'è un solo piccolo problema che ogni tanto sono certa ha introdotto bacherozzi in più di una applicazione, se utilizzo la seconda versione, dato che DataUno e DataDue sono lo stesso oggetto, se cambio il contenuto di DataUno ho cambiato di conseguenza anche DataDue, mentre se utilizzo la prima versione, se cambio DataUno, DataDue rimane quella di prima.
A cosa è dovuto tutto questo? Al fatto che mentre Equals si basa sull'uguaglianza fra gli HashCode degli oggetti, == si basa sulla comparazione dei contenuti. Faccio un esempio pratico così vediamo come realizzare una classe Entity che rispetti le specifiche, cioè si comporti come la nostra classe del Framework standard DateTime.
La classe esempio è naturalmente una classe inutile per il 99% di coloro che leggeranno l'articolo, infatti costruisce un Entity che permette di definire una coppia di colori Console per cambiare colore al testo di una Write o Writeline su una finestra console. Però questa classe è serializzabile, comparabile e sortabile secondo direttive standard.
Per permettere un copia incolla utile in visual studio, ve la scrivo tutta, ma commenterò i pezzi che ci interessano scrivendo in Rosso
#region Using directives
using System;
using System.Collections.Generic;
using System.Text;
using System.Xml.Serialization;
using ForYou.Base;
using ForYou.Base.Xml;
using System.ComponentModel;
using ForYou.Base.Exceptions;
#endregion
namespace ForYou.UI.TheConsole.Data.Entities
{
///
/// Descrizione della classe:
///
///
///
[Serializable, XmlRoot(Namespace = http://www.visual-basic.it)]
//Attributo che indica che la classe è serializzabile e il
namespace univoco per le nostre Entity
public class ConsoleItemColor : INotifyPropertyChanged, IComparable
{
#region Variabili private
///
/// Nome della classe usato per debug nella generazione delle eccezioni
///
///
///
private readonly string mClassName =
System.Reflection.MethodBase.GetCurrentMethod().ReflectedType.Name;
private readonly static string mClassNameS =
System.Reflection.MethodBase.GetCurrentMethod().ReflectedType.Name;
///
/// Colore del testo
///
///
///
private ConsoleColor mTextColor;
///
/// Colore dello schermo
///
///
///
private ConsoleColor mScreenColor;
#endregion
#region Events
///
/// Evento che si scatena alla modifica di una delle proprietà
///
///
///
public event PropertyChangedEventHandler PropertyChanged;
#endregion
#region Costruttore
///
/// Costruttore
///
/// Colore del testo
/// Colore dello schermo
///
///
public ConsoleItemColor( ConsoleColor pTextColor,
ConsoleColor pScreenColor)
{
this.mTextColor = pTextColor;
this.mScreenColor = pScreenColor;
}
///
/// Costruttore
///
///
/// V.1.0
///
private ConsoleItemColor()
{
this.mScreenColor = ConsoleColor.Black;
this.mTextColor = ConsoleColor.White;
}
Una piccola nota sul costruttore privato: questo oggetto deve per forza avere i 2 valori dei colori definiti nel costruttore, però, per essere serializzabile, un oggetto deve implementare un costruttore senza parametri, per evitare che chi usa l'oggetto abbia accesso al costruttore senza parametri, mentre la funzione di Serializzazione e Deserializzazione invece possano utilizzarlo, lo definiamo private.
#endregion
#region Proprietà
///
/// Colore del testo
///
///
///
public ConsoleColor TextColor
{
get
{
return mTextColor;
}
set
{
mTextColor = value;
OnPropertyChanged(new PropertyChangedEventArgs("TextColor"));
}
}
Una nota sull'implementazione di INotifyPropertyChange: è un interfaccia molto utile ed interessante da implementare sugli oggetti Entity per permettere di reagire alla modifica di una property in modo automatico implementando un Event Handler sull'evento Property Changed (ad esempio sulla User interface - ovviamente la console non è il massimo, però...).
///
/// Colore dello schermo
///
///
///
public ConsoleColor ScreenColor
{
get
{
return mScreenColor;
}
set
{
mScreenColor = value;
OnPropertyChanged(new PropertyChangedEventArgs("ScreenColor"));
}
}
///
/// Restituisce il console Item Color corrente con i colori invertiti
///
///
///
public ConsoleItemColor ReverseColor
{
get
{
return new ConsoleItemColor( this.mScreenColor,this.mTextColor);
}
}
#endregion
#region Metodi pubblici
///
/// Operatore di uguaglianza, 2 ConsoleItemColor sono
uguali se hanno gli stessi colori per testo e Schermo
///
/// ConsoleSize da confrontare
/// ConsoleSize di confronto
///
///
///
public static bool operator ==(ConsoleItemColor pX, ConsoleItemColor pY)
{
try
{
bool ret = false;
object oX = (object)pX;
object oY = (object)pY;
if (oX == null && oY == null)
{
ret = true;
}
else
{
if (oX != null && oY != null)
{
if (pX.CompareTo(pY) == 0)
{
ret = true;
} // if
}
}
return (ret);
}
catch (Exception ex)
{
throw new ApplicationException(" " + mClassNameS + "."
+ System.Reflection.MethodBase.GetCurrentMethod().Name + ": " + ex.Message, ex);
}
}
Primo dei metodi per noi interessanti: Implementazione dell'operatore di uguaglianza (==) in questo caso, utilizziamo Object come mezzo per testare e gestire correttamente la comparazione con il null quindi trasformiamo gli operandi in Object per poter usare l'operatore di uguaglianza di base. Se non facessimo così, creeremmo una funzione ricorsiva, (provare per credere) e un bellissimo Stack Overflow.
Se gli operandi non sono nulli, l'uguaglianza esiste solo se la funzione CompareTo implementazione di IComparable ritorna 0. Per il CompareTo, scendete un po' più in basso.
///
/// Operatore di disuguaglianza, 2
ConsoleItemColor sono diversi se non sono uguali
///
/// ConsoleSize da confrontare
/// ConsoleSize di confronto
///
///
///
public static bool operator !=(ConsoleItemColor pX, ConsoleItemColor pY)
{
try
{
return (!(pX == pY));
}
catch (Exception ex)
{
throw new ApplicationException(" " + mClassNameS + "."
+ System.Reflection.MethodBase.GetCurrentMethod().Name, ex);
}
}
Come possiamo notare, la disuguaglianza è solo una negazione dell'uguaglianza, pertanto la sua descrizione è corretta, due ConsoleItemColor sono diversi se non sono uguali... :o)
///
/// Indica se 2 instanze di oggetti sono uguali
///
/// Oggetto da comparare
///
/// true se obj e this sono lo stesso oggetto e hanno lo stesso valore
/// false al contrario.
///
public override bool Equals(object obj)
{
bool ret = false;
try
{
if( obj == null )
{
ret = false;
}
else
{
if (obj is ConsoleItemColor)
{
ret = this.GetHashCode() == obj.GetHashCode();
} // if
} // else
}
catch (Exception ex)
{
if( ExceptionHelper.BaseExceptionTypeEquals(ex,
typeof(NullReferenceException) ))
{
if( obj == null )
{
ret = true;
}
}
else
{
throw new ApplicationException(" " + mClassName + "."
+ System.Reflection.MethodBase.GetCurrentMethod().Name, ex);
}
}
return( ret );
}
Anche l'implementazione di Equals è un po' più lunga di una semplice uguaglianza tra HashCode, questo per riuscire a gestire correttamente gli oggetti null, ovviamente se non volete gestire la nullabilità togliete tutto quello che ho scritto e lasciate che Equals lanci una bella NullReferenceException gestendone il risultato a monte.
///
/// Restituisce lo hash code dell'instanza dell'oggetto
///
///
/// Un intero a 32-bit che contiene l'hash code dell'instanza dell'oggetto
///
public override int GetHashCode()
{
try
{
return(base.GetHashCode());
}
catch (Exception ex)
{
throw new ApplicationException(" " + mClassName + "."
+ System.Reflection.MethodBase.GetCurrentMethod().Name, ex);
}
}
Contrariamente a quanto indicato nel post precedente, che è una delle modalità per gestire gli Hash, ho scelto di implementare semplicemente la chiamata allo Hash della classe base (Object) perché in questo modo sono certa che Equals funziona come voglio io, ovvero ritorna True solo se gli oggetti comparati sono lo stesso oggetto.
///
/// Visualizza il contenuto della classe come una stringa
///
///
///
public override string ToString()
{
try
{
StringBuilder sb = new StringBuilder();
sb.AppendFormat("{0};{1}", this.TextColor, this.ScreenColor);
return(sb.ToString());
}
catch (Exception ex)
{
throw new ApplicationException(" " + mClassName + "."
+ System.Reflection.MethodBase.GetCurrentMethod().Name + ": " + ex.Message, ex);
}
}
///
/// Serializza la classe su un file in formato XML
///
///Path del file da generare
///
public void WriteXml(string pXmlPath)
{
try
{
Stat.CheckEmptyPath(pXmlPath);
ObjSerializer so = new ObjSerializer();
so.SerializeToFile(pXmlPath, this, typeof(ConsoleItemColor));
}
catch (Exception ex)
{
throw new ApplicationException(" " + this.mClassName + "."
+ System.Reflection.MethodBase.GetCurrentMethod().Name + " " + ex.Message, ex);
}
}
///
/// Serializza la classe su una stringa
///
/// Stringa XML contenente l'oggetto serializzato
///
public string WriteXml()
{
try
{
ObjSerializer so = new ObjSerializer();
return (so.SerializeToString(this));
}
catch (Exception ex)
{
throw new ApplicationException(" " + this.mClassName + "."
+ System.Reflection.MethodBase.GetCurrentMethod().Name + " " + ex.Message, ex);
}
}
///
/// Deserializza una classe da un file XML
///
/// Dati Base x la deserializzazione
/// if set to true [pXML è un oggetto serializzato] se false è un path.
/// Ritorna: Un oggetto di tipo MenuItem
///
///
public static ConsoleItemColor ReadXml(string pXml, bool pIsXmlData)
{
ConsoleItemColor ret = null;
try
{
ObjSerializer so = new ObjSerializer();
if (!pIsXmlData)
{
Stat.CheckEmptyPath(pXml);
Stat.CheckNoFile(pXml);
ret = (ConsoleItemColor)so.DeserializeFromFile(
typeof(ConsoleItemColor), pXml);
} // if
else
{
ret = (ConsoleItemColor)so.DeserializeFromString(
typeof(ConsoleItemColor), pXml);
}
}
catch (Exception ex)
{
throw new ApplicationException(" " + mClassNameS + "."
+ System.Reflection.MethodBase.GetCurrentMethod().Name + " " + ex.Message, ex);
}
return (ret);
}
#endregion
#region INotifyPropertyChanged Implementazione event handler
///
/// Evento che viene generato
///
/// Argomenti relativi all'evento
///
///
protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
{
if (PropertyChanged != null)
PropertyChanged(this, e);
}
#endregion
#region IComparable Members
///
/// Compara 2 item color
///
///
///
public int CompareTo(object obj)
{
int ret = -1;
try
{
if (obj is ConsoleItemColor)
{
ConsoleItemColor val = (ConsoleItemColor)obj;
int textCompare = this.TextColor.CompareTo(val.TextColor);
int screenCompare = this.ScreenColor.CompareTo(val.ScreenColor);
if (textCompare != 0)
{
ret = textCompare;
}
else
{
ret = screenCompare;
}
}
}
catch (Exception ex)
{
throw new ApplicationException(" " + mClassName + "."
+ System.Reflection.MethodBase.GetCurrentMethod().Name, ex);
}
return (ret);
}
E qui abbiamo la nostra ultima fatica, ovvero l'implementazione di CompareTo, per comparare 2 oggetti e definirli uguali, siamo noi a decidere come procedere, in questo caso, due oggetti di tipo ConsoleItemColor sono uguali se hanno lo stesso colore per il testo e lo stesso colore per lo schermo. Inoltre abbiamo stabilito che un ConsoleItemColor è Maggiore di un'altro se ha il colore del Testo maggiore di quello dell'item con cui è comparato, quando i colori del testo fossero uguali, allora il colore dello schermo maggiore ci indica chi è il maggiore dei due. Come stabilire se un colore è maggiore o minore? I ConsoleColor sono una enumerazione intera che va da 0 a 15 pertanto stabiliamo la loro dimensione cercandone il valore numerico.
#endregion
}
}
Conclusione: Questo è ovviamente solo un modo per implementare gli operatori di Uguaglianza e di Precedenza su un oggetto di tipo Entity, ciascuno può scegliere di implementarli come preferisce, è necessario però prestare attenzione a come si scrivono questi metodi affinché l'oggetto si comporti poi come vogliamo noi all'interno di un programma.
Tags: GetHashCode, Classi