Megjegyzés
Az oldalhoz való hozzáféréshez engedély szükséges. Megpróbálhat bejelentkezni vagy módosítani a címtárat.
Az oldalhoz való hozzáféréshez engedély szükséges. Megpróbálhatja módosítani a címtárat.
Jótanács
Először érdemes rekordokat használni. A rekordok automatikusan minimális kóddal implementálják az értékegyenlőséget, így a legtöbb adatközpontú típushoz ajánlott megközelítés. Ha egyéni értékegyenlőségi logikára van szüksége, vagy nem tudja használni a rekordokat, folytassa az alábbi manuális megvalósítási lépésekkel.
Amikor osztályt vagy szerkezetet határoz meg, eldönti, hogy érdemes-e egyéni értékegyenlőséget (vagy egyenértékűséget) létrehozni a típushoz. Általában akkor valósítja meg az értékegyenlőséget, ha egy gyűjteményhez ilyen típusú objektumokat szeretne hozzáadni, vagy amikor elsődleges célja mezők vagy tulajdonságok készletének tárolása. Az értékegyenlőség definícióját a típus összes mezőjének és tulajdonságainak összehasonlítására alapozhatja, vagy a definíciót egy részhalmazra alapozhatja.
Mindkét esetben, és mindkét osztályban és szerkezetben a megvalósításnak az öt egyenértékűségi garanciát kell követnie (a következő szabályok esetében feltételezze, hogy x, y és z nem null értékűek):
A reflexív tulajdonság:
x.Equals(x)visszaadjatrue.A szimmetrikus tulajdonság:
x.Equals(y)ugyanazt az értéket adja vissza, mint ay.Equals(x).A tranzitív tulajdonság: ha
(x.Equals(y) && y.Equals(z))visszaadjatrue-t, akkorx.Equals(z)visszaadjatrue-t.Az egymást követő meghívások
x.Equals(y)ugyanazt az értéket adják vissza, amíg az x és az y által hivatkozott objektumok nem módosulnak.A nem null értékű értékek nem egyenlők a null értékkel. Null
x.Equals(y)érték eseténxazonban kivételt eredményez. Ez az 1-es vagy a 2-es szabályt sérti, attól függően, hogy mi az argumentum aEqualsszámára.
Az Ön által definiált bármely struktúra már rendelkezik az értékegyenlőség alapértelmezett implementációjával, amelyet a System.ValueType metódus Object.Equals(Object) felülbírálásából örököl. Ez az implementáció tükröződés segítségével vizsgálja meg a típus összes mezőjét és tulajdonságát. Bár ez az implementáció helyes eredményeket hoz létre, viszonylag lassú a kifejezetten a típushoz írt egyéni implementációhoz képest.
Az értékegyenlőség implementálási részletei az osztályok és a szerkezetek esetében eltérőek. Az osztályok és a szerkezetek azonban ugyanazokat az alapvető lépéseket igénylik az egyenlőség megvalósításához:
Bírálja felül a virtuálisObject.Equals(Object) metódust. Ez polimorfikus egyenlőségi viselkedést biztosít, lehetővé téve az objektumok megfelelő összehasonlítását, amikor
objecthivatkozásként kezeljük őket. Biztosítja a megfelelő viselkedést a gyűjteményekben és a polimorfizmus használatakor. A legtöbb esetben az önbool Equals( object obj )implementációjának csak aEqualsfelület megvalósítását végző, típusspecifikus System.IEquatable<T> metódust kell meghívnia. (Lásd a 2. lépést.)Implementálja az System.IEquatable<T> interfészt egy típusspecifikus
Equalsmetódus megadásával. Ez a típusbiztos egyenlőség-ellenőrzést biztosítja boxolás nélkül, ami jobb teljesítményt eredményez. Emellett elkerüli a szükségtelen típusátalakítást, és lehetővé teszi a fordítási idejű típusellenőrzést. Itt történik a tényleges egyenértékűség-összehasonlítás. Dönthet például úgy, hogy egyenlőséget határoz meg, ha csak egy vagy két mezőt hasonlít össze a típusában. Ne dobjon kivételeket a kódrészletbőlEquals. Örökléssel összefüggő osztályok esetén:Ennek a módszernek csak az osztályban deklarált mezőket kell megvizsgálnia. Meg kell hívnia
base.Equals, hogy vizsgálja meg az alaposztályban lévő mezőket. (Ne hívja megbase.Equals, ha a típus közvetlenül örököl Object, mert a Object végrehajtása referenciális egyenlőség-ellenőrzést hajt végre Object.Equals(Object).)Két változó csak akkor tekinthető egyenlőnek, ha az összehasonlítandó változók futásidejű típusai megegyeznek. Győződjön meg arról is, hogy a
IEquatableEqualsfutási idő típusának metódusa akkor lesz implementálva, ha egy változó futásideje és fordítási ideje eltérő. Az egyik stratégia annak biztosítására, hogy a futásidejű típusok mindig helyesen legyenek összehasonlítva, az, hogy aIEquatableosztályokban csak asealedkerüljön implementálásra. További információkért tekintse meg a cikk későbbi részében található osztály példáját .
Nem kötelező, de ajánlott: Túlterhelje a == és a != operátorokat. Ez konzisztens és intuitív szintaxist biztosít az egyenlőségi összehasonlításokhoz, amelyek megfelelnek a beépített típusok felhasználói elvárásainak. Ez biztosítja ezt,
obj1 == obj2ésobj1.Equals(obj2)ugyanúgy viselkedik.Bíráld felül a Object.GetHashCode elemet, hogy két, értékegyenlőséggel rendelkező objektum ugyanazt a kivonatkódot hozzon létre. Ez szükséges a kivonatalapú gyűjtemények ( például
Dictionary<TKey,TValue>ésHashSet<T>) helyes viselkedéséhez. Az egyenlő objektumoknak egyenlő kivonatkódokkal kell rendelkezniük, vagy ezek a gyűjtemények nem fognak megfelelően működni.Nem kötelező: A "nagyobb, mint" vagy a "kisebb, mint" definíciók támogatásához implementálja a IComparable<T> interfészt a típusához, és túlterhelje az <= és az >= operátorokat is. Ez lehetővé teszi a rendezési műveleteket, és teljes rendezési kapcsolatot biztosít a típushoz, ami akkor hasznos, ha objektumokat ad hozzá a rendezett gyűjteményekhez, vagy tömbök vagy listák rendezésekor.
Rekord példa
Az alábbi példa bemutatja, hogy a rekordok hogyan valósítják meg automatikusan az értékegyenlőséget minimális kóddal. Az első rekord TwoDPoint egy egyszerű rekordtípus, amely automatikusan megvalósítja az értékegyenlőséget. A második rekord ThreeDPoint azt mutatja be, hogy a rekordok más rekordokból is származtathatók, és továbbra is fenntartják a megfelelő értékegyenlőségi viselkedést:
namespace ValueEqualityRecord;
public record TwoDPoint(int X, int Y);
public record ThreeDPoint(int X, int Y, int Z) : TwoDPoint(X, Y);
class Program
{
static void Main(string[] args)
{
// Create some points
TwoDPoint pointA = new TwoDPoint(3, 4);
TwoDPoint pointB = new TwoDPoint(3, 4);
TwoDPoint pointC = new TwoDPoint(5, 6);
ThreeDPoint point3D_A = new ThreeDPoint(3, 4, 5);
ThreeDPoint point3D_B = new ThreeDPoint(3, 4, 5);
ThreeDPoint point3D_C = new ThreeDPoint(3, 4, 7);
Console.WriteLine("=== Value Equality with Records ===");
// Value equality works automatically
Console.WriteLine($"pointA.Equals(pointB) = {pointA.Equals(pointB)}"); // True
Console.WriteLine($"pointA == pointB = {pointA == pointB}"); // True
Console.WriteLine($"pointA.Equals(pointC) = {pointA.Equals(pointC)}"); // False
Console.WriteLine($"pointA == pointC = {pointA == pointC}"); // False
Console.WriteLine("\n=== Hash Codes ===");
// Equal objects have equal hash codes automatically
Console.WriteLine($"pointA.GetHashCode() = {pointA.GetHashCode()}");
Console.WriteLine($"pointB.GetHashCode() = {pointB.GetHashCode()}");
Console.WriteLine($"pointC.GetHashCode() = {pointC.GetHashCode()}");
Console.WriteLine("\n=== Inheritance with Records ===");
// Inheritance works correctly with value equality
Console.WriteLine($"point3D_A.Equals(point3D_B) = {point3D_A.Equals(point3D_B)}"); // True
Console.WriteLine($"point3D_A == point3D_B = {point3D_A == point3D_B}"); // True
Console.WriteLine($"point3D_A.Equals(point3D_C) = {point3D_A.Equals(point3D_C)}"); // False
// Different types are not equal (unlike problematic class example)
Console.WriteLine($"pointA.Equals(point3D_A) = {pointA.Equals(point3D_A)}"); // False
Console.WriteLine("\n=== Collections ===");
// Works seamlessly with collections
var pointSet = new HashSet<TwoDPoint> { pointA, pointB, pointC };
Console.WriteLine($"Set contains {pointSet.Count} unique points"); // 2 unique points
var pointDict = new Dictionary<TwoDPoint, string>
{
{ pointA, "First point" },
{ pointC, "Different point" }
};
// Demonstrate that equivalent points work as the same key
var duplicatePoint = new TwoDPoint(3, 4);
Console.WriteLine($"Dictionary contains key for {duplicatePoint}: {pointDict.ContainsKey(duplicatePoint)}"); // True
Console.WriteLine($"Dictionary contains {pointDict.Count} entries"); // 2 entries
Console.WriteLine("\n=== String Representation ===");
// Automatic ToString implementation
Console.WriteLine($"pointA.ToString() = {pointA}");
Console.WriteLine($"point3D_A.ToString() = {point3D_A}");
Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
}
/* Expected Output:
=== Value Equality with Records ===
pointA.Equals(pointB) = True
pointA == pointB = True
pointA.Equals(pointC) = False
pointA == pointC = False
=== Hash Codes ===
pointA.GetHashCode() = -1400834708
pointB.GetHashCode() = -1400834708
pointC.GetHashCode() = -148136000
=== Inheritance with Records ===
point3D_A.Equals(point3D_B) = True
point3D_A == point3D_B = True
point3D_A.Equals(point3D_C) = False
pointA.Equals(point3D_A) = False
=== Collections ===
Set contains 2 unique points
Dictionary contains key for TwoDPoint { X = 3, Y = 4 }: True
Dictionary contains 2 entries
=== String Representation ===
pointA.ToString() = TwoDPoint { X = 3, Y = 4 }
point3D_A.ToString() = ThreeDPoint { X = 3, Y = 4, Z = 5 }
*/
A rekordok számos előnnyel járnak az értékegyenlőség szempontjából:
-
Automatikus megvalósítás: A rekordok automatikusan implementálják System.IEquatable<T> és felülbírálják Object.Equals az Object.GetHashCode
==/!=operátorokat. -
Helyes öröklési viselkedés: A rekordok olyan virtuális metódusokkal implementálhatók
IEquatable<T>, amelyek ellenőrzik mindkét operandus futtatókörnyezeti típusát, biztosítva a helyes viselkedést az öröklési hierarchiákban és a polimorf forgatókönyvekben. - Nem módosíthatóság alapértelmezés szerint: A rekordok nem módosítható kialakítást ösztönöznek, ami jól működik az értékegyenlőség szemantikájával.
- Tömör szintaxis: A pozícióparaméterek kompakt módot biztosítanak az adattípusok definiálására.
- Jobb teljesítmény: A fordító által létrehozott egyenlőségi implementáció optimalizálva van, és nem használja a tükrözést, mint az alapértelmezett struct implementáció.
Rekordokat akkor használjon, ha az elsődleges cél az adatok tárolása, és értékegyenlőségi szemantikára van szüksége.
Hivatkozási egyenlőséget használó tagokkal rendelkező rekordok
Ha a rekordok olyan tagokat tartalmaznak, amelyek hivatkozási egyenlőséget használnak, a rekordok automatikus értékegyenlőségi viselkedése nem a várt módon működik. Ez olyan gyűjteményekre vonatkozik, mint a System.Collections.Generic.List<T>tömbök és más referenciatípusok, amelyek nem implementálják az értékalapú egyenlőséget (a jelentős kivétellel System.String, amely az értékegyenlőséget valósítja meg).
Fontos
Bár a rekordok kiváló értékegyenlőséget biztosítanak az alapszintű adattípusokhoz, nem oldják meg automatikusan az értékegyenlőséget a hivatkozási egyenlőséget használó tagok számára. Ha egy rekord olyan , System.Arrayvagy más hivatkozástípust System.Collections.Generic.List<T>tartalmaz, amely nem valósít meg értékegyenlőséget, akkor a tagokban azonos tartalommal rendelkező két rekordpéldány továbbra sem lesz egyenlő, mert a tagok hivatkozási egyenlőséget használnak.
public record PersonWithHobbies(string Name, List<string> Hobbies);
var person1 = new PersonWithHobbies("Alice", new List<string> { "Reading", "Swimming" });
var person2 = new PersonWithHobbies("Alice", new List<string> { "Reading", "Swimming" });
Console.WriteLine(person1.Equals(person2)); // False - different List instances!
Ennek az az oka, hogy a rekordok az Object.Equals egyes tagok metódusát használják, és a gyűjteménytípusok általában a hivatkozási egyenlőséget használják a tartalom összehasonlítása helyett.
Az alábbiakban látható a probléma:
// Records with reference-equality members don't work as expected
public record PersonWithHobbies(string Name, List<string> Hobbies);
A kód futtatásakor a következő módon viselkedik:
Console.WriteLine("=== Records with Collections - The Problem ===");
// Problem: Records with mutable collections use reference equality for the collection
var person1 = new PersonWithHobbies("Alice", [ "Reading", "Swimming" ]);
var person2 = new PersonWithHobbies("Alice", [ "Reading", "Swimming" ]);
Console.WriteLine($"person1: {person1}");
Console.WriteLine($"person2: {person2}");
Console.WriteLine($"person1.Equals(person2): {person1.Equals(person2)}"); // False! Different List instances
Console.WriteLine($"Lists have same content: {person1.Hobbies.SequenceEqual(person2.Hobbies)}"); // True
Console.WriteLine();
Megoldások referenciaegyenlőség-tagokkal rendelkező rekordokhoz
Egyéni System.IEquatable<T> implementáció: Cserélje le a fordító által létrehozott egyenlőséget egy kézzel kódolt verzióra, amely tartalomalapú összehasonlítást biztosít a referencia-egyenlőségi tagok számára. Gyűjtemények esetén elemenkénti összehasonlítást vagy hasonló metódusokat alkalmazhat Enumerable.SequenceEqual .
Lehetőség szerint használjon értéktípusokat: Fontolja meg, hogy az adatok megjeleníthetők-e olyan értéktípusokkal vagy nem módosítható struktúrákkal, amelyek természetesen támogatják az értékegyenlőséget, például System.Numerics.Vector<T> vagy Plane.
Értékalapú egyenlőséget használó típusok használata: Gyűjtemények esetén érdemes lehet olyan típusokat használni, amelyek értékalapú egyenlőséget valósítanak meg, vagy olyan egyéni gyűjteménytípusokat implementálnak, amelyek felülbírálják Object.Equals a tartalomalapú összehasonlítást, például System.Collections.Immutable.ImmutableArray<T> vagy System.Collections.Immutable.ImmutableList<T>.
Tervezés a referenciaegyenlőség szem előtt tartásával: Fogadja el, hogy egyes tagok a referenciaegyenlőséget fogják használni, és ennek megfelelően tervezik meg az alkalmazás logikáját, biztosítva, hogy ugyanazokat a példányokat használja újra, amikor fontos az egyenlőség.
Íme egy példa a gyűjteményekkel rendelkező rekordok egyéni egyenlőségének megvalósítására:
// A potential solution using IEquatable<T> with custom equality
public record PersonWithHobbiesFixed(string Name, List<string> Hobbies) : IEquatable<PersonWithHobbiesFixed>
{
public virtual bool Equals(PersonWithHobbiesFixed? other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
// Use SequenceEqual for List comparison
return Name == other.Name && Hobbies.SequenceEqual(other.Hobbies);
}
public override int GetHashCode()
{
// Create hash based on content, not reference
var hashCode = new HashCode();
hashCode.Add(Name);
foreach (var hobby in Hobbies)
{
hashCode.Add(hobby);
}
return hashCode.ToHashCode();
}
}
Ez az egyéni implementáció megfelelően működik:
Console.WriteLine("=== Solution 1: Custom IEquatable Implementation ===");
var personFixed1 = new PersonWithHobbiesFixed("Bob", [ "Cooking", "Hiking" ]);
var personFixed2 = new PersonWithHobbiesFixed("Bob", [ "Cooking", "Hiking" ]);
Console.WriteLine($"personFixed1: {personFixed1}");
Console.WriteLine($"personFixed2: {personFixed2}");
Console.WriteLine($"personFixed1.Equals(personFixed2): {personFixed1.Equals(personFixed2)}"); // True! Custom equality
Console.WriteLine();
Ugyanez a probléma a tömböket és más gyűjteménytípusokat is érinti:
// These also use reference equality - the issue persists
public record PersonWithHobbiesArray(string Name, string[] Hobbies);
public record PersonWithHobbiesImmutable(string Name, IReadOnlyList<string> Hobbies);
A tömbök hivatkozási egyenlőséget is használnak, és ugyanazokat a váratlan eredményeket eredményezik:
Console.WriteLine("=== Arrays Also Use Reference Equality ===");
var personArray1 = new PersonWithHobbiesArray("Charlie", ["Gaming", "Music" ]);
var personArray2 = new PersonWithHobbiesArray("Charlie", ["Gaming", "Music" ]);
Console.WriteLine($"personArray1: {personArray1}");
Console.WriteLine($"personArray2: {personArray2}");
Console.WriteLine($"personArray1.Equals(personArray2): {personArray1.Equals(personArray2)}"); // False! Arrays use reference equality too
Console.WriteLine($"Arrays have same content: {personArray1.Hobbies.SequenceEqual(personArray2.Hobbies)}"); // True
Console.WriteLine();
Még az írásvédett gyűjtemények is ezt a referenciaegyenlőségi viselkedést mutatják:
Console.WriteLine("=== Same Issue with IReadOnlyList ===");
var personImmutable1 = new PersonWithHobbiesImmutable("Diana", [ "Art", "Travel" ]);
var personImmutable2 = new PersonWithHobbiesImmutable("Diana", [ "Art", "Travel" ]);
Console.WriteLine($"personImmutable1: {personImmutable1}");
Console.WriteLine($"personImmutable2: {personImmutable2}");
Console.WriteLine($"personImmutable1.Equals(personImmutable2): {personImmutable1.Equals(personImmutable2)}"); // False! Reference equality
Console.WriteLine($"Content is the same: {personImmutable1.Hobbies.SequenceEqual(personImmutable2.Hobbies)}"); // True
Console.WriteLine();
A legfontosabb megállapítás az, hogy a rekordok megoldják a strukturális egyenlőség problémáját, de nem módosítják a bennük található típusok szemantikai egyenlőségi viselkedését.
Osztály példa
Az alábbi példa bemutatja, hogyan valósíthat meg értékegyenlőséget egy osztályban (referenciatípus). Ez a manuális megközelítés akkor szükséges, ha nem tud rekordokat használni, vagy egyéni egyenlőségi logikára van szüksége:
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) = {pointA.Equals(pointB)}");
Console.WriteLine($"pointA == pointB = {pointA == pointB}");
Console.WriteLine($"null comparison = {pointA.Equals(pointC)}");
Console.WriteLine($"Compare to some other type = {pointA.Equals(i)}");
TwoDPoint pointD = null;
TwoDPoint pointE = null;
Console.WriteLine($"Two null TwoDPoints are equal: {pointD == pointE}");
pointE = new TwoDPoint(3, 4);
Console.WriteLine($"(pointE == pointA) = {pointE == pointA}");
Console.WriteLine($"(pointA == pointE) = {pointA == pointE}");
Console.WriteLine($"(pointA != pointE) = {pointA != pointE}");
System.Collections.ArrayList list = new System.Collections.ArrayList();
list.Add(new ThreeDPoint(3, 4, 5));
Console.WriteLine($"pointE.Equals(list[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
*/
Az osztályok (referenciatípusok) esetében mindkét Object.Equals(Object) módszer alapértelmezett implementációja a referenciaegyenlítési összehasonlítást hajtja végre, nem értékegyenlőség-ellenőrzést. Ha egy implementáló felülírja a virtuális módszert, a cél az, hogy értéket adjon neki az egyenlőség szemantikájának.
Az == osztályokkal és != operátorokkal akkor is használható, ha az osztály nem túlterheli őket. Az alapértelmezett viselkedés azonban a referencia-egyenlőség ellenőrzése. Egy osztályban, ha túlterheli a Equals metódust, akkor érdemes lenne túlterhelni a == és != operátorokat is, de ez nem kötelező.
Fontos
Előfordulhat, hogy az előző példakód nem minden öröklési forgatókönyvet a várt módon kezel. Tekintse meg az alábbi kódot:
TwoDPoint p1 = new ThreeDPoint(1, 2, 3);
TwoDPoint p2 = new ThreeDPoint(1, 2, 4);
Console.WriteLine(p1.Equals(p2)); // output: True
Ez a kód azt jelzi, hogy p1 egyenlő p2-vel, annak ellenére, hogy különbség van a z értékek között. A különbség figyelmen kívül marad, mert a fordító a fordítási időbeli típus alapján választja ki a TwoDPoint megvalósítást IEquatable. Ez alapvető probléma az öröklési hierarchiák polimorfikus egyenlőségével kapcsolatban.
Polimorfikus egyenlőség
Ha értékegyenlőséget valósít meg az osztályokkal rendelkező öröklési hierarchiákban, az osztálypéldában bemutatott standard megközelítés helytelen viselkedéshez vezethet, ha az objektumokat polimorfikusan használják. A probléma azért fordul elő, mert System.IEquatable<T> a implementációk fordítási idő alapján vannak kiválasztva, nem pedig futtatókörnyezet típusa alapján.
A standard implementációkkal kapcsolatos probléma
Fontolja meg ezt a problémás forgatókönyvet:
TwoDPoint p1 = new ThreeDPoint(1, 2, 3); // Declared as TwoDPoint
TwoDPoint p2 = new ThreeDPoint(1, 2, 4); // Declared as TwoDPoint
Console.WriteLine(p1.Equals(p2)); // True - but should be False!
Az összehasonlítás azért ad True vissza, mert a fordító a deklarált típus alapján választ, TwoDPoint.Equals(TwoDPoint) figyelmen kívül hagyva a Z koordináta-különbségeket.
A polimorfikus egyenlőség javításának kulcsa annak biztosítása, hogy minden egyenlőségi összehasonlítás a virtuális Object.Equals módszert használja, amely képes ellenőrizni a futtatókörnyezet típusait, és megfelelően kezelni az öröklést. Ez úgy érhető el, hogy explicit felületi implementációt használ az adott meghatalmazottak számára System.IEquatable<T> a virtuális módszerhez:
Az alaposztály a kulcsmintákat mutatja be:
// Safe polymorphic equality implementation using explicit interface implementation
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) => Equals(obj as TwoDPoint);
// Explicit interface implementation prevents compile-time type issues
bool IEquatable<TwoDPoint>.Equals(TwoDPoint? p) => Equals((object?)p);
protected virtual 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);
}
A származtatott osztály megfelelően kiterjeszti az egyenlőség logikáját:
// 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) => Equals(obj as ThreeDPoint);
// Explicit interface implementation prevents compile-time type issues
bool IEquatable<ThreeDPoint>.Equals(ThreeDPoint? p) => Equals((object?)p);
protected override bool Equals(TwoDPoint? p)
{
if (p is null)
{
return false;
}
// Optimization for a common success case.
if (Object.ReferenceEquals(this, p))
{
return true;
}
// Runtime type check happens in the base method
if (p is ThreeDPoint threeD)
{
// Check properties that this class declares.
if (Z != threeD.Z)
{
return false;
}
return base.Equals(p);
}
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);
}
Az implementáció így kezeli a problémás polimorfikus forgatókönyveket:
Console.WriteLine("=== Safe Polymorphic Equality ===");
// Test polymorphic scenarios that were problematic before
TwoDPoint p1 = new ThreeDPoint(1, 2, 3);
TwoDPoint p2 = new ThreeDPoint(1, 2, 4);
TwoDPoint p3 = new ThreeDPoint(1, 2, 3);
TwoDPoint p4 = new TwoDPoint(1, 2);
Console.WriteLine("Testing polymorphic equality (declared as TwoDPoint):");
Console.WriteLine($"p1 = ThreeDPoint(1, 2, 3) as TwoDPoint");
Console.WriteLine($"p2 = ThreeDPoint(1, 2, 4) as TwoDPoint");
Console.WriteLine($"p3 = ThreeDPoint(1, 2, 3) as TwoDPoint");
Console.WriteLine($"p4 = TwoDPoint(1, 2)");
Console.WriteLine();
Console.WriteLine($"p1.Equals(p2) = {p1.Equals(p2)}"); // False - different Z values
Console.WriteLine($"p1.Equals(p3) = {p1.Equals(p3)}"); // True - same values
Console.WriteLine($"p1.Equals(p4) = {p1.Equals(p4)}"); // False - different types
Console.WriteLine($"p4.Equals(p1) = {p4.Equals(p1)}"); // False - different types
Console.WriteLine();
Az implementáció a közvetlen típus-összehasonlításokat is megfelelően kezeli:
// Test direct type comparisons
var point3D_A = new ThreeDPoint(3, 4, 5);
var point3D_B = new ThreeDPoint(3, 4, 5);
var point3D_C = new ThreeDPoint(3, 4, 7);
var point2D_A = new TwoDPoint(3, 4);
Console.WriteLine("Testing direct type comparisons:");
Console.WriteLine($"point3D_A.Equals(point3D_B) = {point3D_A.Equals(point3D_B)}"); // True
Console.WriteLine($"point3D_A.Equals(point3D_C) = {point3D_A.Equals(point3D_C)}"); // False
Console.WriteLine($"point3D_A.Equals(point2D_A) = {point3D_A.Equals(point2D_A)}"); // False
Console.WriteLine($"point2D_A.Equals(point3D_A) = {point2D_A.Equals(point3D_A)}"); // False
Console.WriteLine();
Az egyenlőség megvalósítása a gyűjteményekkel is megfelelően működik:
// Test with collections
Console.WriteLine("Testing with collections:");
var hashSet = new HashSet<TwoDPoint> { p1, p2, p3, p4 };
Console.WriteLine($"HashSet contains {hashSet.Count} unique points"); // Should be 3: one ThreeDPoint(1,2,3), one ThreeDPoint(1,2,4), one TwoDPoint(1,2)
var dictionary = new Dictionary<TwoDPoint, string>
{
{ p1, "First 3D point" },
{ p2, "Second 3D point" },
{ p4, "2D point" }
};
Console.WriteLine($"Dictionary contains {dictionary.Count} entries");
Console.WriteLine($"Dictionary lookup for equivalent point: {dictionary.ContainsKey(new ThreeDPoint(1, 2, 3))}"); // True
Az előző kód az értékalapú egyenlőség megvalósításának legfontosabb elemeit mutatja be:
-
Virtuális
Equals(object?)felülbírálás: A fő egyenlőségi logika a virtuális Object.Equals metódusban történik, amelyet fordítási idő típusától függetlenül hívunk. -
Futásidejű típusellenőrzés: A használata
this.GetType() != p.GetType()biztosítja, hogy a különböző típusú objektumok soha ne legyenek egyenlők. - Explicit felület implementálása: A System.IEquatable<T> implementáció delegálja a virtuális metódust, megakadályozva a fordítási idő típusú kiválasztási problémákat.
-
Védett virtuális segédmetódus: A
protected virtual Equals(TwoDPoint? p)metódus lehetővé teszi, hogy a származtatott osztályok felülbírálják az egyenlőség logikáját a típusbiztonság fenntartása mellett.
Használja ezt a mintát, ha:
- Olyan öröklési hierarchiák vannak, amelyekben fontos az értékegyenlőség
- Az objektumok polimorfikusan használhatók (alaptípusként deklarálva, származtatott típusként példányosítva)
- Értékegyenlőség szemantikával rendelkező referenciatípusokra van szüksége
Az előnyben részesített megközelítés az értékalapú egyenlőség megvalósítása típusok használata record . Ez a megközelítés összetettebb megvalósítást igényel, mint a standard megközelítés, és a helyesség biztosítása érdekében a polimorf forgatókönyvek alapos tesztelését igényli.
Példa strukturálásra
Az alábbi példa bemutatja, hogyan valósíthatja meg az értékegyenlőséget egy szerkezetben (értéktípus). Bár a szerkezetek alapértelmezett értékegyenlőséggel rendelkeznek, az egyéni megvalósítás javíthatja a teljesítményt:
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) = {pointA.Equals(pointB)}");
// True:
Console.WriteLine($"pointA == pointB = {pointA == pointB}");
// True:
Console.WriteLine($"object.Equals(pointA, pointB) = {object.Equals(pointA, pointB)}");
// False:
Console.WriteLine($"pointA.Equals(null) = {pointA.Equals(null)}");
// False:
Console.WriteLine($"(pointA == null) = {pointA == null}");
// True:
Console.WriteLine($"(pointA != null) = {pointA != null}");
// False:
Console.WriteLine($"pointA.Equals(i) = {pointA.Equals(i)}");
// CS0019:
// Console.WriteLine($"pointA == i = {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]): {pointA.Equals(list[0])}");
// Compare nullable to nullable and to non-nullable.
TwoDPoint? pointC = null;
TwoDPoint? pointD = null;
// False:
Console.WriteLine($"pointA == (pointC = null) = {pointA == pointC}");
// True:
Console.WriteLine($"pointC == pointD = {pointC == pointD}");
TwoDPoint temp = new TwoDPoint(3, 4);
pointC = temp;
// True:
Console.WriteLine($"pointA == (pointC = 3,4) = {pointA == pointC}");
pointD = temp;
// True:
Console.WriteLine($"pointD == (pointC = 3,4) = {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
*/
}
A szerkezetek esetében az alapértelmezett implementáció Object.Equals(Object) (amely a System.ValueType felülírt verziója) reflektálást használ az értékek összehasonlítására, hogy ellenőrizze a típus minden mezőjének értékegyenlőségét. Bár ez az implementáció helyes eredményeket hoz létre, viszonylag lassú a kifejezetten a típushoz írt egyéni implementációhoz képest.
Ha felülbírálja a virtuális Equals metódust egy szerkezetben, az a cél, hogy hatékonyabb eszközt biztosítson az értékegyenlőség-ellenőrzés végrehajtásához, és opcionálisan az összehasonlítást az struktúra mezőinek vagy tulajdonságainak bizonyos részhalmazára alapozza.
Az == és != operátorok csak akkor működhetnek a szerkezeten, ha a szerkezet explicit módon túlterheli őket.