Definování rovnosti hodnot pro třídu nebo strukturu (Průvodce programováním v C#)
Záznamy automaticky implementují rovnost hodnot. Zvažte definování namísto record
class
toho, kdy data modelů typů a měla by implementovat rovnost hodnot.
Při definování třídy nebo struktury se rozhodnete, zda má smysl vytvořit vlastní definici rovnosti hodnot (nebo ekvivalence) pro typ. Rovnost hodnot obvykle implementujete, když očekáváte, že do kolekce přidáte objekty typu nebo když jejich primárním účelem je uložit sadu polí nebo vlastností. Definici rovnosti hodnot můžete založit na porovnání všech polí a vlastností v typu nebo můžete definici založit na podmnožině.
V obou případech a v obou třídách a strukturách by vaše implementace měla dodržovat pět záruk ekvivalence (pro následující pravidla předpokládejme, že x
y
a z
nejsou null):
Reflexní vlastnost:
x.Equals(x)
vrátítrue
.Symetrická vlastnost:
x.Equals(y)
vrátí stejnou hodnotu jakoy.Equals(x)
.Tranzitivní vlastnost: pokud
(x.Equals(y) && y.Equals(z))
vrátítrue
, pakx.Equals(z)
vrátítrue
.Následné vyvolání
x.Equals(y)
vrátí stejnou hodnotu, pokud objekty, na které odkazuje x a y, se nezmění.Jakákoli hodnota, která není null, se nerovná hodnotě null. Vyvolá však výjimku,
x.Equals(y)
pokudx
je null. Tím se přeruší pravidla 1 nebo 2 v závislosti na argumentuEquals
.
Každá struktura, kterou definujete, již má výchozí implementaci rovnosti hodnoty, kterou dědí z System.ValueType přepsání Object.Equals(Object) metody. Tato implementace používá reflexi k prozkoumání všech polí a vlastností v typu. I když tato implementace vytváří správné výsledky, je relativně pomalé v porovnání s vlastní implementací, kterou píšete speciálně pro daný typ.
Podrobnosti implementace rovnosti hodnot se liší pro třídy a struktury. Obě třídy i struktury však vyžadují pro implementaci rovnosti stejný základní postup:
Přepište virtuální Object.Equals(Object) metodu. Ve většině případů by vaše implementace
bool Equals( object obj )
měla pouze zavolat do metody specifické proEquals
typ, která je implementací System.IEquatable<T> rozhraní. (Viz krok 2.)System.IEquatable<T> Implementujte rozhraní zadáním metody specifické pro typ
Equals
. Tady se provádí skutečné porovnání ekvivalence. Můžete se například rozhodnout definovat rovnost porovnáním pouze jednoho nebo dvou polí ve vašem typu. Nevyvolávejte výjimky zEquals
. Třídy, které souvisejí dědičností:Tato metoda by měla zkoumat pouze pole deklarovaná ve třídě. Měla by volat
base.Equals
zkoumání polí, která jsou v základní třídě. (Nevolejtebase.Equals
, pokud typ dědí přímo z Object, protože Object implementace Object.Equals(Object) provádí kontrolu rovnosti odkazů.)Dvě proměnné by měly být považovány za stejné, pouze pokud jsou porovnávané typy proměnných za běhu stejné. Také se ujistěte, že
IEquatable
implementaceEquals
metody pro typ běhu je použita, pokud jsou typy runtime a kompilace proměnné odlišné. Jednou strategií pro zajištění správného porovnání typů za běhu je implementaceIEquatable
pouze vesealed
třídách. Další informace najdete v příkladu třídy dále v tomto článku.
Přepsat Object.GetHashCode tak, aby dva objekty, které mají rovnost hodnot, vytvořily stejný hash kód.
Volitelné: Chcete-li podporovat definice pro "větší než" nebo "menší než", implementujte IComparable<T> rozhraní pro váš typ a také přetěžujte <operátory = a >= .
Poznámka:
Záznamy můžete použít k získání sémantiky rovnosti hodnot bez zbytečného často používaného kódu.
Příklad třídy
Následující příklad ukazuje, jak implementovat rovnost hodnot ve třídě (typ odkazu).
namespace ValueEqualityClass;
class TwoDPoint : IEquatable<TwoDPoint>
{
public int X { get; private set; }
public int Y { get; private set; }
public TwoDPoint(int x, int y)
{
if (x is (< 1 or > 2000) || y is (< 1 or > 2000))
{
throw new ArgumentException("Point must be in range 1 - 2000");
}
this.X = x;
this.Y = y;
}
public override bool Equals(object obj) => this.Equals(obj as TwoDPoint);
public bool Equals(TwoDPoint p)
{
if (p is null)
{
return false;
}
// Optimization for a common success case.
if (Object.ReferenceEquals(this, p))
{
return true;
}
// If run-time types are not exactly the same, return false.
if (this.GetType() != p.GetType())
{
return false;
}
// Return true if the fields match.
// Note that the base class is not invoked because it is
// System.Object, which defines Equals as reference equality.
return (X == p.X) && (Y == p.Y);
}
public override int GetHashCode() => (X, Y).GetHashCode();
public static bool operator ==(TwoDPoint lhs, TwoDPoint rhs)
{
if (lhs is null)
{
if (rhs is null)
{
return true;
}
// Only the left side is null.
return false;
}
// Equals handles case of null on right side.
return lhs.Equals(rhs);
}
public static bool operator !=(TwoDPoint lhs, TwoDPoint rhs) => !(lhs == rhs);
}
// For the sake of simplicity, assume a ThreeDPoint IS a TwoDPoint.
class ThreeDPoint : TwoDPoint, IEquatable<ThreeDPoint>
{
public int Z { get; private set; }
public ThreeDPoint(int x, int y, int z)
: base(x, y)
{
if ((z < 1) || (z > 2000))
{
throw new ArgumentException("Point must be in range 1 - 2000");
}
this.Z = z;
}
public override bool Equals(object obj) => this.Equals(obj as ThreeDPoint);
public bool Equals(ThreeDPoint p)
{
if (p is null)
{
return false;
}
// Optimization for a common success case.
if (Object.ReferenceEquals(this, p))
{
return true;
}
// Check properties that this class declares.
if (Z == p.Z)
{
// Let base class check its own fields
// and do the run-time type comparison.
return base.Equals((TwoDPoint)p);
}
else
{
return false;
}
}
public override int GetHashCode() => (X, Y, Z).GetHashCode();
public static bool operator ==(ThreeDPoint lhs, ThreeDPoint rhs)
{
if (lhs is null)
{
if (rhs is null)
{
// null == null = true.
return true;
}
// Only the left side is null.
return false;
}
// Equals handles the case of null on right side.
return lhs.Equals(rhs);
}
public static bool operator !=(ThreeDPoint lhs, ThreeDPoint rhs) => !(lhs == rhs);
}
class Program
{
static void Main(string[] args)
{
ThreeDPoint pointA = new ThreeDPoint(3, 4, 5);
ThreeDPoint pointB = new ThreeDPoint(3, 4, 5);
ThreeDPoint pointC = null;
int i = 5;
Console.WriteLine("pointA.Equals(pointB) = {0}", pointA.Equals(pointB));
Console.WriteLine("pointA == pointB = {0}", pointA == pointB);
Console.WriteLine("null comparison = {0}", pointA.Equals(pointC));
Console.WriteLine("Compare to some other type = {0}", pointA.Equals(i));
TwoDPoint pointD = null;
TwoDPoint pointE = null;
Console.WriteLine("Two null TwoDPoints are equal: {0}", pointD == pointE);
pointE = new TwoDPoint(3, 4);
Console.WriteLine("(pointE == pointA) = {0}", pointE == pointA);
Console.WriteLine("(pointA == pointE) = {0}", pointA == pointE);
Console.WriteLine("(pointA != pointE) = {0}", pointA != pointE);
System.Collections.ArrayList list = new System.Collections.ArrayList();
list.Add(new ThreeDPoint(3, 4, 5));
Console.WriteLine("pointE.Equals(list[0]): {0}", pointE.Equals(list[0]));
// Keep the console window open in debug mode.
Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
}
/* Output:
pointA.Equals(pointB) = True
pointA == pointB = True
null comparison = False
Compare to some other type = False
Two null TwoDPoints are equal: True
(pointE == pointA) = False
(pointA == pointE) = False
(pointA != pointE) = True
pointE.Equals(list[0]): False
*/
U tříd (odkazových typů) výchozí implementace obou Object.Equals(Object) metod provádí porovnání rovnosti odkazů, nikoli kontroly rovnosti hodnot. Když implementátor přepíše virtuální metodu, účelem je dát jí sémantiku rovnosti.
Operátory ==
a !=
operátory lze použít s třídami, i když je třída nepřetíží. Výchozí chování je však provést kontrolu rovnosti odkazů. Pokud v třídě přetížíte metodu Equals
, měli byste přetížit operátory ==
a !=
operátory, ale není to nutné.
Důležité
Předchozí ukázkový kód nemusí zpracovávat každý scénář dědičnosti očekávaným způsobem. Uvažujte následující kód:
TwoDPoint p1 = new ThreeDPoint(1, 2, 3);
TwoDPoint p2 = new ThreeDPoint(1, 2, 4);
Console.WriteLine(p1.Equals(p2)); // output: True
Tento kód hlásí, že p1
se rovná p2
navzdory rozdílu v z
hodnotách. Rozdíl je ignorován, protože kompilátor vybere TwoDPoint
implementaci IEquatable
na základě typu kompilace.
Předdefinovaná rovnost hodnot typů record
zpracovává podobné scénáře správně. Pokud TwoDPoint
a ThreeDPoint
byly record
typy, výsledek p1.Equals(p2)
by byl False
. Další informace naleznete v tématu Rovnost v record
hierarchiích dědičnosti typů.
Příklad struktury
Následující příklad ukazuje, jak implementovat rovnost hodnot ve struktuře (typ hodnoty):
namespace ValueEqualityStruct
{
struct TwoDPoint : IEquatable<TwoDPoint>
{
public int X { get; private set; }
public int Y { get; private set; }
public TwoDPoint(int x, int y)
: this()
{
if (x is (< 1 or > 2000) || y is (< 1 or > 2000))
{
throw new ArgumentException("Point must be in range 1 - 2000");
}
X = x;
Y = y;
}
public override bool Equals(object? obj) => obj is TwoDPoint other && this.Equals(other);
public bool Equals(TwoDPoint p) => X == p.X && Y == p.Y;
public override int GetHashCode() => (X, Y).GetHashCode();
public static bool operator ==(TwoDPoint lhs, TwoDPoint rhs) => lhs.Equals(rhs);
public static bool operator !=(TwoDPoint lhs, TwoDPoint rhs) => !(lhs == rhs);
}
class Program
{
static void Main(string[] args)
{
TwoDPoint pointA = new TwoDPoint(3, 4);
TwoDPoint pointB = new TwoDPoint(3, 4);
int i = 5;
// True:
Console.WriteLine("pointA.Equals(pointB) = {0}", pointA.Equals(pointB));
// True:
Console.WriteLine("pointA == pointB = {0}", pointA == pointB);
// True:
Console.WriteLine("object.Equals(pointA, pointB) = {0}", object.Equals(pointA, pointB));
// False:
Console.WriteLine("pointA.Equals(null) = {0}", pointA.Equals(null));
// False:
Console.WriteLine("(pointA == null) = {0}", pointA == null);
// True:
Console.WriteLine("(pointA != null) = {0}", pointA != null);
// False:
Console.WriteLine("pointA.Equals(i) = {0}", pointA.Equals(i));
// CS0019:
// Console.WriteLine("pointA == i = {0}", pointA == i);
// Compare unboxed to boxed.
System.Collections.ArrayList list = new System.Collections.ArrayList();
list.Add(new TwoDPoint(3, 4));
// True:
Console.WriteLine("pointA.Equals(list[0]): {0}", pointA.Equals(list[0]));
// Compare nullable to nullable and to non-nullable.
TwoDPoint? pointC = null;
TwoDPoint? pointD = null;
// False:
Console.WriteLine("pointA == (pointC = null) = {0}", pointA == pointC);
// True:
Console.WriteLine("pointC == pointD = {0}", pointC == pointD);
TwoDPoint temp = new TwoDPoint(3, 4);
pointC = temp;
// True:
Console.WriteLine("pointA == (pointC = 3,4) = {0}", pointA == pointC);
pointD = temp;
// True:
Console.WriteLine("pointD == (pointC = 3,4) = {0}", pointD == pointC);
Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
}
/* Output:
pointA.Equals(pointB) = True
pointA == pointB = True
Object.Equals(pointA, pointB) = True
pointA.Equals(null) = False
(pointA == null) = False
(pointA != null) = True
pointA.Equals(i) = False
pointE.Equals(list[0]): True
pointA == (pointC = null) = False
pointC == pointD = True
pointA == (pointC = 3,4) = True
pointD == (pointC = 3,4) = True
*/
}
U struktur provádí výchozí implementace Object.Equals(Object) (což je přepsáná verze) System.ValueTypekontrolu rovnosti hodnot pomocí reflexe k porovnání hodnot každého pole v typu. Pokud implementátor přepíše virtuální Equals
metodu ve struktuře, je účelem poskytnout efektivnější způsob provádění kontroly rovnosti hodnot a volitelně založit porovnání na některých podmnožinách polí nebo vlastností struktury.
Operátory == a != nemohou pracovat se strukturou, pokud je struktura explicitně nepřetěžuje.