Notitie
Voor toegang tot deze pagina is autorisatie vereist. U kunt proberen u aan te melden of de directory te wijzigen.
Voor toegang tot deze pagina is autorisatie vereist. U kunt proberen de mappen te wijzigen.
Aanbeveling
Overweeg eerst records te gebruiken. Records implementeren automatisch gelijkheid van waarden met minimale code, waardoor ze de aanbevolen benadering zijn voor de meeste gegevensgerichte typen. Als u aangepaste logica voor waarde gelijkheid nodig hebt of records niet kunt gebruiken, gaat u verder met de onderstaande handmatige implementatiestappen.
Wanneer u een klasse of struct definieert, bepaalt u of het zinvol is om een aangepaste definitie van waarde gelijkheid (of gelijkwaardigheid) te maken voor het type. Normaal gesproken implementeert u gelijkheid van waarden wanneer u verwacht objecten van het type aan een verzameling toe te voegen of wanneer het primaire doel is om een set velden of eigenschappen op te slaan. U kunt uw definitie van waarde gelijkheid baseren op een vergelijking van alle velden en eigenschappen in het type, of u kunt de definitie baseren op een subset.
In beide gevallen en in beide klassen en structs moet uw implementatie de vijf garanties van gelijkwaardigheid volgen (voor de volgende regels wordt ervan uitgegaan dat xen yz niet null zijn):
De reflexieve eigenschap:
x.Equals(x)retourneerttrue.De symmetrische eigenschap:
x.Equals(y)retourneert dezelfde waarde alsy.Equals(x).De transitieve eigenschap: als
(x.Equals(y) && y.Equals(z))retourneerttrue, dan retourneertx.Equals(z)true.Opeenvolgende aanroepen van
x.Equals(y)retourneren dezelfde waarde zolang de objecten waarnaar wordt verwezen door x en y niet worden gewijzigd.Een niet-null-waarde is niet gelijk aan null.
x.Equals(y)Genereert echter een uitzondering wanneerxnull is. Dat breekt regels 1 of 2, afhankelijk van het argument aanEquals.
Elke struct die u definieert, heeft al een standaardimplementatie van waardegelijkheid die wordt overgenomen van de System.ValueType overschrijving van de Object.Equals(Object) methode. Deze implementatie maakt gebruik van weerspiegeling om alle velden en eigenschappen in het type te onderzoeken. Hoewel deze implementatie correcte resultaten oplevert, is het relatief traag vergeleken met een aangepaste implementatie die u specifiek schrijft voor het type.
De implementatiedetails voor waarde gelijkheid verschillen voor klassen en structs. Voor zowel klassen als structs zijn echter dezelfde basisstappen vereist voor het implementeren van gelijkheid:
De virtueleObject.Equals(Object) methode overschrijven. Dit biedt polymorf gelijkheidsgedrag, zodat uw objecten correct kunnen worden vergeleken wanneer ze als
objectverwijzingen worden behandeld. Het zorgt voor correct gedrag in verzamelingen en bij het gebruik van polymorfisme. In de meeste gevallen moet uw implementatiebool Equals( object obj )alleen de typespecifiekeEqualsmethode aanroepen die de implementatie van de System.IEquatable<T> interface is. (Zie stap 2.)Implementeer de System.IEquatable<T> interface door een typespecifieke
Equalsmethode te bieden. Dit biedt typeveilige gelijkheidscontrole zonder boksen, wat resulteert in betere prestaties. Het voorkomt ook onnodige cast-conversie en maakt het controleren van het type compileertijd mogelijk. Hier wordt de daadwerkelijke gelijkwaardigheidsvergelijking uitgevoerd. U kunt bijvoorbeeld besluiten gelijkheid te definiëren door slechts één of twee velden in uw type te vergelijken. Gooi geen uitzonderingen vanEquals. Voor klassen die gerelateerd zijn door overerving:Met deze methode moeten alleen velden worden onderzocht die in de klasse zijn gedeclareerd. Er moet een aanroep
base.Equalsworden uitgevoerd om velden te onderzoeken die zich in de basisklasse bevinden. (Roep nietbase.Equalsaan als het type rechtstreeks overgaat van Object, omdat de Object implementatie van Object.Equals(Object) een verwijzing gelijkheidscontrole uitvoert.)Twee variabelen moeten alleen gelijk worden beschouwd als de runtimetypen van de variabelen die worden vergeleken, hetzelfde zijn. Zorg er ook voor dat de implementatie van de
IEquatableEqualsmethode voor het runtimetype wordt gebruikt als de runtime- en compileertijdtypen van een variabele verschillen. Een strategie om ervoor te zorgen dat runtimetypen altijd correct worden vergeleken, is omIEquatablealleen in desealedklassen te implementeren. Zie het voorbeeld van de klas verderop in dit artikel voor meer informatie.
Optioneel maar aanbevolen: Overbelast de == en != operators. Dit biedt consistente en intuïtieve syntaxis voor gelijkheidsvergelijkingen, overeenkomende gebruikersverwachtingen van ingebouwde typen. Het zorgt ervoor dat
obj1 == obj2enobj1.Equals(obj2)zich op dezelfde manier gedragen.Overschrijven Object.GetHashCode zodat twee objecten met waarde-gelijkheid dezelfde hash-code produceren. Dit is vereist voor het juiste gedrag in hash-verzamelingen zoals
Dictionary<TKey,TValue>enHashSet<T>. Objecten die gelijk zijn, moeten gelijke hash-codes hebben of deze verzamelingen werken niet goed.Optioneel: Ter ondersteuning van definities voor 'groter dan' of 'kleiner dan', implementeer de IComparable<T> interface voor uw type, en overlaad ook de <= en >= operatoren. Dit maakt sorteerbewerkingen mogelijk en biedt een volledige volgorderelatie voor uw type, handig bij het toevoegen van objecten aan gesorteerde verzamelingen of bij het sorteren van matrices of lijsten.
Recordvoorbeeld
In het volgende voorbeeld ziet u hoe records automatisch gelijkheid van waarden met minimale code implementeren. De eerste record TwoDPoint is een eenvoudig recordtype dat automatisch gelijkheid van waarden implementeert. De tweede record ThreeDPoint laat zien dat records kunnen worden afgeleid van andere records en nog steeds het juiste gedrag van gelijkheid van waarde behouden:
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 }
*/
Records bieden verschillende voordelen voor waarde gelijkheid:
-
Automatische implementatie: records implementeren System.IEquatable<T> en negerenObject.EqualsObject.GetHashCode, en de
==/!=operators. -
Correct overnamegedrag: records implementeren
IEquatable<T>met behulp van virtuele methoden die het runtimetype van beide operanden controleren, waardoor correct gedrag in overnamehiërarchieën en polymorfe scenario's wordt gegarandeerd. - Onveranderbaarheid standaard: records stimuleren onveranderbaar ontwerp, wat goed werkt met semantiek voor waarde-gelijkheid.
- Beknopte syntaxis: Positionele parameters bieden een compacte manier om gegevenstypen te definiëren.
- Betere prestaties: de door compiler gegenereerde gelijkheids-implementatie is geoptimaliseerd en maakt geen gebruik van weerspiegeling zoals de standaardstruct-implementatie.
Gebruik records wanneer uw primaire doel is om gegevens op te slaan en u waarde-semantiek voor gelijkheid nodig hebt.
Records met leden die verwijzen naar gelijkheid
Wanneer records leden bevatten die verwijzen naar gelijkheid, werkt het gedrag van automatische waarde gelijkheid van records niet zoals verwacht. Dit geldt voor verzamelingen zoals System.Collections.Generic.List<T>matrices en andere referentietypen die geen gelijkheid op basis van waarden implementeren (met uitzondering van System.String, die wel gelijkheid van waarden implementeert).
Belangrijk
Records bieden uitstekende gelijkheid van waarden voor basisgegevenstypen, maar ze lossen waarde-gelijkheid niet automatisch op voor leden die verwijzen naar gelijkheid. Als een record een System.Collections.Generic.List<T>, System.Arrayof andere verwijzingstypen bevat die geen gelijkheid van waarden implementeren, zijn twee recordexemplaren met identieke inhoud in die leden nog steeds niet gelijk omdat de leden verwijzings gelijkheid gebruiken.
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!
Dit komt doordat records de Object.Equals methode van elk lid gebruiken en verzamelingstypen doorgaans verwijzen naar gelijkheid in plaats van hun inhoud te vergelijken.
Hieronder ziet u het probleem:
// Records with reference-equality members don't work as expected
public record PersonWithHobbies(string Name, List<string> Hobbies);
Dit gedraagt zich als volgt wanneer u de code uitvoert:
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();
Oplossingen voor records met leden voor referentie-gelijkheid
Aangepaste System.IEquatable<T> implementatie: Vervang de door de compiler gegenereerde gelijkheid door een handgecodeerde versie die inhoudsgebaseerde vergelijking biedt voor leden van referentie-gelijkheid. Implementeer voor verzamelingen een vergelijking van elementen per element met behulp van Enumerable.SequenceEqual of vergelijkbare methoden.
Gebruik waar mogelijk waardetypen: Overweeg of uw gegevens kunnen worden weergegeven met waardetypen of onveranderbare structuren die natuurlijk ondersteuning bieden voor gelijkheid van waarden, zoals System.Numerics.Vector<T> of Plane.
Typen gebruiken met gelijkheid op basis van waarden: Voor verzamelingen kunt u overwegen om typen te gebruiken die gelijkheid op basis van waarden implementeren of aangepaste verzamelingstypen implementeren die worden overschreven Object.Equals om vergelijking op basis van inhoud te bieden, zoals System.Collections.Immutable.ImmutableArray<T> of System.Collections.Immutable.ImmutableList<T>.
Ontwerp met referentie gelijkheid in gedachten: accepteer dat sommige leden referentiegewijs gelijkheid gebruiken en uw toepassingslogica dienovereenkomstig ontwerpen, zodat u dezelfde exemplaren opnieuw gebruikt wanneer gelijkheid belangrijk is.
Hier volgt een voorbeeld van het implementeren van aangepaste gelijkheid voor records met verzamelingen:
// 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();
}
}
Deze aangepaste implementatie werkt correct:
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();
Hetzelfde probleem is van invloed op matrices en andere verzamelingstypen:
// These also use reference equality - the issue persists
public record PersonWithHobbiesArray(string Name, string[] Hobbies);
public record PersonWithHobbiesImmutable(string Name, IReadOnlyList<string> Hobbies);
Matrices maken ook gebruik van referentie gelijkheid, waardoor dezelfde onverwachte resultaten worden gegenereerd:
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();
Zelfs leesverzamelingen vertonen dit referentiegedrag voor gelijkheid:
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();
Het belangrijkste inzicht is dat records het structurele gelijkheidsprobleem oplossen, maar niet het semantische gelijkheidsgedrag wijzigen van de typen die ze bevatten.
Voorbeeld van klasse
In het volgende voorbeeld ziet u hoe u gelijkheid van waarden in een klasse (verwijzingstype) implementeert. Deze handmatige aanpak is nodig wanneer u geen records kunt gebruiken of aangepaste gelijkheidslogica nodig hebt:
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
*/
Bij klassen (referentietypen) voert de standaard implementatie van beide Object.Equals(Object) methoden een vergelijking van referentie gelijkheid uit, niet een waarde gelijkheidscontrole. Wanneer een implementator de virtuele methode overschrijft, is het doel om het waarden-gelijkheidssemantiek te geven.
De == en != operators kunnen worden gebruikt met klassen, zelfs als de klassen ze niet overloaden. Het standaardgedrag is echter om een referentie-gelijkheidscontrole uit te voeren. Als u de Equals methode in een klasse overbelast, moet u de == en != operators overbelasten, maar dit is niet vereist.
Belangrijk
De voorgaande voorbeeldcode verwerkt mogelijk niet elk overnamescenario zoals u verwacht. Kijk eens naar de volgende code:
TwoDPoint p1 = new ThreeDPoint(1, 2, 3);
TwoDPoint p2 = new ThreeDPoint(1, 2, 4);
Console.WriteLine(p1.Equals(p2)); // output: True
Deze code rapporteert dat p1 gelijk is aan p2 ondanks het verschil in z waarden. Het verschil wordt genegeerd omdat de compiler de implementatie van TwoDPoint kiest op basis van het compileertijdtype van IEquatable. Dit is een fundamenteel probleem met polymorfe gelijkheid in overnamehiërarchieën.
Polymorfische gelijkheid
Bij het implementeren van gelijkheid van waarden in overnamehiërarchieën met klassen kan de standaardbenadering in het klassevoorbeeld leiden tot onjuist gedrag wanneer objecten polymorf worden gebruikt. Het probleem treedt op omdat System.IEquatable<T> implementaties worden gekozen op basis van het type compileertijd, niet het runtimetype.
Het probleem met standaard implementaties
Houd rekening met dit problematische scenario:
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!
De vergelijking retourneert True omdat de compiler selecteert TwoDPoint.Equals(TwoDPoint) op basis van het gedeclareerde type, waarbij de Z coördinaatverschillen worden genegeerd.
De sleutel voor het corrigeren van polymorfe gelijkheid is ervoor zorgen dat alle gelijkheidsvergelijkingen gebruikmaken van de virtuele Object.Equals methode, die runtimetypen kan controleren en overname correct kan verwerken. Dit kan worden bereikt met behulp van expliciete interface-implementatie voor System.IEquatable<T> die gedelegeerden aan de virtuele methode:
De basisklasse demonstreert de belangrijkste patronen:
// 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);
}
De afgeleide klasse breidt de gelijkheidslogica correct uit:
// 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);
}
Dit is de manier waarop deze implementatie de problematische polymorfe scenario's verwerkt:
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();
De implementatie verwerkt ook direct typevergelijkingen correct:
// 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();
De gelijkheidsuitvoering werkt ook goed met verzamelingen:
// 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
De voorgaande code demonstreert belangrijke elementen voor het implementeren van gelijkheid op basis van waarde:
-
Virtuele
Equals(object?)onderdrukking: de belangrijkste gelijkheidslogica vindt plaats in de virtuele Object.Equals methode, die wordt aangeroepen, ongeacht het type compileertijd. -
Controle van runtimetypen: Door
this.GetType() != p.GetType()te gebruiken wordt ervoor gezorgd dat objecten van verschillende typen nooit als gelijk worden beschouwd. - Expliciete interface-implementatie: de System.IEquatable<T> implementatie delegeert aan de virtuele methode, waardoor selectieproblemen met het type compileertijd worden voorkomen.
-
Beveiligde virtuele helpermethode: met de
protected virtual Equals(TwoDPoint? p)methode kunnen afgeleide klassen gelijkheidslogica overschrijven terwijl de veiligheid van het type behouden blijft.
Gebruik dit patroon wanneer:
- U hebt overnamehiërarchieën waarbij gelijkheid van waarden belangrijk is
- Objecten kunnen polymorf worden gebruikt (gedeclareerd als basistype, geïnstantieerd als afgeleid type)
- U hebt referentietypen met semantiek voor waarde gelijkheid nodig
De voorkeursbenadering is het gebruik record van typen voor het implementeren van gelijkheid op basis van waarden. Deze aanpak vereist een complexere implementatie dan de standaardbenadering en vereist grondige tests van polymorfe scenario's om de juistheid te waarborgen.
Voorbeeld van een struct
In het volgende voorbeeld ziet u hoe u gelijkheid van waarden implementeert in een struct (waardetype). Hoewel structs standaardwaarde gelijkheid hebben, kan een aangepaste implementatie de prestaties verbeteren:
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
*/
}
Voor structs voert de standaard implementatie van Object.Equals(Object) (de overschreven versie in System.ValueType) een waarde gelijkheidscontrole uit met behulp van weerspiegeling om de waarden van elk veld in het type te vergelijken. Hoewel deze implementatie correcte resultaten oplevert, is het relatief traag vergeleken met een aangepaste implementatie die u specifiek schrijft voor het type.
Wanneer u de virtuele Equals methode in een struct overschrijft, is het doel om een efficiëntere manier te bieden om de gelijkheidscontrole van waarden uit te voeren en eventueel de vergelijking te baseren op een deelverzameling van de velden of eigenschappen van de struct.
De == operatoren en != kunnen niet op een struct werken, tenzij de struct deze expliciet overbelast.