Jazyk C# rozlišuje dva typy porovnávání - porovnání referenční (reference equality) a hodnotové (value equality). Hodnotové porovnávání se chápe tak, že dva objekty jsou shodné pokud tyto objekty představují stejnou hodnotu, referenční se chápe tak, že dvě reference jsou shodné pokud odkazují na stejný objekt (tj. stejnou instanci). Pro referenční porovnávání se používá metoda ReferenceEquals, pro hodnotové metoda Equals (případně statická metoda Equals).
Pokud referenční typ hodnotové porovnávání neimplementuje provádí metoda Equals i operátory == (rovno) a != (nerovno) pouze porovnávání referenční.
Hodnotové porovnávání by měli implementovat objekty, jejíž vnitřní data sémanticky představují nějakou hodnotu, tj. měli by přepsat metodu Equals a GetHashCode a přetížit operátor == a operátor !=. POZOR ale, že tyto objekty by také měli být vždy implementovány jako immutable tj. aby se jejich vnitřní stav (hodnota) nemohl v průběhu života instance měnit.
Rozdíl obou typů porovnávání u typu implementujícího hodnotové porovnávání (zde třída Point) můžeme demonstrovat například takto:
static class Test
{
static void Main()
{
Point p1 = new Point(1, 3);
Point p2 = new Point(1, 3);
Console.WriteLine(object.ReferenceEquals(p1, p2)); //False
Console.WriteLine(p1 == p2); //True
}
}
A jak správně a optimálně hodnotové porovnávání implementovat?
Ukážeme si to na jednoduchém příkladu výše zmíněné třídy Point obsahující pouze dvě vlastnosti X a Y určující hodnotu instance.
Implementace této třídy bude vypadat takto:
[System.Diagnostics.DebuggerDisplay("\\{ X = {X}, Y = {Y} \\}")]
public class Point
{
#region member varible and default property initialization
public int X { get; private set; }
public int Y { get; private set; }
#endregion
#region constructors and destructors
public Point(int x, int y)
{
this.X = x;
this.Y = y;
}
#endregion
#region action methods
public override bool Equals(object obj)
{
if (obj == null)
{
return false;
}
var other = obj as Point;
if ((object)other == null)
{
return false;
}
return this == other;
}
public bool Equals(Point other)
{
if ((object)other == null)
{
return false;
}
return this == other;
}
public override int GetHashCode()
{
return this.X.GetHashCode() ^ this.Y.GetHashCode();
}
public static bool operator ==(Point left, Point right)
{
if (object.ReferenceEquals(left, right))
{
return true;
}
if ((object)left == null || (object)right == null)
{
return false;
}
return left.X == right.X && left.Y == right.Y;
}
public static bool operator !=(Point left, Point right)
{
return !(left == right);
}
public override string ToString()
{
return string.Format("X = {0}, Y = {1}", this.X, this.Y);
}
#endregion
}
Poznámky k implementaci:
- Datový typ Point je immutable a vlastnosti X a Y se nastavují pouze v konstruktoru třídy.
- Metoda Equals neporovnává vlastnosti X a Y instancí other a this, ale namísto toho používá přetížený operátor ==. Je to z toho důvodu, aby vlastní “logika” srovnání dvou instancí byla jen na jednom místě (to by mělo hlavně význam pokud by vlastností určující hodnotu instance bylo větší množství).
- Kromě přepsání metody Equals(object) se z důvodu zvýšení výkonu doporučuje doplnit ještě i metodu Equals(Type).
- Porovnávání na null v metodách Equals(object), Equals(Type) a v operátoru == se musí provádět včetně přetypování na object. Je to z toho důvodu, aby se nevolal náš přetížený operátor ==. Druhou variantou je používat volání metody ReferenceEquals.
- Metoda GetHashCode používá operátor ^ pro “sloučení” dílčích výsledků volání GetHashCode na jednotlivých vlastnostech určujících hodnotu instance (více o tom jak implementovat metodu GetHashCode je popsáno zde).
- Pokud má třída malý počet vlastností určující hodnotu instance (tj. jeden nebo maximálně dvě), nevadilo by operátor == případně implementovat pouze jako jediný výraz:
public static bool operator ==(Point left, Point right)
{
return ((object)left == null && (object)right == null) || ((object)left != null && (object)right != null && left.X == right.X && left.Y == right.Y);
}
U implementace porovnání pro hodnotové datové typy (struktury) odpadá nutnost porovnávání na hodnotu null, s tím jsme se již setkali v článku zde.