Anteckning
Åtkomst till den här sidan kräver auktorisering. Du kan prova att logga in eller ändra kataloger.
Åtkomst till den här sidan kräver auktorisering. Du kan prova att ändra kataloger.
Tips/Råd
Överväg att använda poster först. Poster implementerar automatiskt värdejämlikhet med minimal kod, vilket gör dem till den rekommenderade metoden för de flesta datafokuserade typer. Om du behöver logik för anpassad värdejämlikhet eller inte kan använda poster fortsätter du med stegen för manuell implementering nedan.
När du definierar en klass eller struct bestämmer du om det är lämpligt att skapa en anpassad definition av värdejämlikhet (eller likvärdighet) för typen. Vanligtvis implementerar du värdejämlikhet när du förväntar dig att lägga till objekt av typen i en samling, eller när deras primära syfte är att lagra en uppsättning fält eller egenskaper. Du kan basera definitionen av värdejämlikhet på en jämförelse av alla fält och egenskaper i typen, eller så kan du basera definitionen på en delmängd.
I båda fallen och i både klasser och structs bör implementeringen följa de fem garantierna för likvärdighet (för följande regler antar du att x, y och z inte är null):
Den reflexiva egenskapen:
x.Equals(x)returnerartrue.Den symmetriska egenskapen:
x.Equals(y)returnerar samma värde somy.Equals(x).Den transitiva egenskapen: om
(x.Equals(y) && y.Equals(z))returnerartruereturnerarx.Equals(z).trueEfterföljande anrop av
x.Equals(y)returnerar samma värde så länge objekten som refereras av x och y inte ändras.Alla värden som inte är null är inte lika med null. Genererar dock
x.Equals(y)ett undantag närxär null. Det bryter mot regler 1 eller 2, beroende på argumentet tillEquals.
Alla strukturer som du definierar har redan en standardimplementering av värdejämlikhet som den ärver från en åsidosättning av metoden System.ValueTypeObject.Equals(Object). Den här implementeringen använder reflektion för att undersöka alla fält och egenskaper i typen. Även om den här implementeringen ger korrekta resultat är den relativt långsam jämfört med en anpassad implementering som du skriver specifikt för typen.
Implementeringsinformationen för värdejämlikhet skiljer sig åt för klasser och structs. Både klasser och structs kräver dock samma grundläggande steg för att implementera likhet:
Åsidosätt den virtuellaObject.Equals(Object) metoden. Detta ger polymorft likhetsbeteende, vilket gör att dina objekt kan jämföras korrekt när de behandlas som
objectreferenser. Det säkerställer korrekt beteende i samlingar och när du använder polymorfism. I de flesta fall bör implementeringen avbool Equals( object obj )bara anropa den typspecifikaEqualsmetod som är implementeringen av System.IEquatable<T> gränssnittet. (Se steg 2.)System.IEquatable<T> Implementera gränssnittet genom att ange en typspecifik
Equalsmetod. Detta ger typsäker likhetskontroll utan boxning, vilket resulterar i bättre prestanda. Det undviker också onödig gjutning och möjliggör kompileringstidstypkontroll. Det är här som den faktiska jämförelsen av ekvivalens utförs. Du kan till exempel välja att definiera likhet genom att bara jämföra ett eller två fält i din typ. Kasta inte undantag frånEquals. För klasser som är relaterade till arv:Den här metoden bör endast undersöka fält som deklareras i klassen. Den bör anropa
base.Equalsför att undersöka fält som finns i basklassen. (Anropabase.Equalsinte om typen ärver direkt från Object, eftersom implementeringen Object av Object.Equals(Object) utför en referensjämlikhetskontroll.)Två variabler bör anses vara lika endast om körtyperna för variablerna som jämförs är desamma. Kontrollera också att implementeringen
IEquatableav metodenEqualsför körtidstypen används om körtids- och kompileringstyperna för en variabel skiljer sig. En strategi för att säkerställa att körningstider alltid jämförs korrekt är att implementeraIEquatableendast isealed-klasser. Mer information finns i klassexemplet senare i den här artikeln.
Valfritt men rekommenderas: Överbelasta operatorerna == och != . Detta ger konsekvent och intuitiv syntax för likhetsjämförelser som matchar användarens förväntningar från inbyggda typer. Det säkerställer det
obj1 == obj2ochobj1.Equals(obj2)beter sig på samma sätt.Åsidosätt Object.GetHashCode så att två objekt som har värdejämlikhet skapar samma hash-kod. Detta krävs för korrekt beteende i hash-baserade samlingar som
Dictionary<TKey,TValue>ochHashSet<T>. Objekt som är lika med måste ha lika med hashkoder, annars fungerar inte dessa samlingar korrekt.Valfritt: Om du vill stödja definitioner för "större än" eller "mindre än" implementerar du IComparable<T> gränssnittet för din typ och överbelastar även operatorerna< = och >= . Detta möjliggör sorteringsåtgärder och ger en fullständig ordningsrelation för din typ, användbar när du lägger till objekt i sorterade samlingar eller vid sortering av matriser eller listor.
Postexempel
I följande exempel visas hur poster automatiskt implementerar värdejämlikhet med minimal kod. Den första posten TwoDPoint är en enkel posttyp som automatiskt implementerar värdejämlikhet. Den andra posten ThreeDPoint visar att poster kan härledas från andra poster och fortfarande upprätthålla rätt beteende för värdejämlikhet:
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 }
*/
Poster ger flera fördelar för värdejämlikhet:
-
Automatisk implementering: Poster implementerar System.IEquatable<T> och åsidosätter Object.Equalsautomatiskt , Object.GetHashCodeoch operatorerna
==/!=. -
Rätt arvsbeteende: Poster implementerar
IEquatable<T>med hjälp av virtuella metoder som kontrollerar körningstypen för båda operanderna, vilket säkerställer korrekt beteende i arvshierarkier och polymorfa scenarier. - Oföränderlighet som standard: Poster uppmuntrar oföränderlig design, vilket fungerar bra med värdejämlikhetssemantik.
- Koncis syntax: Positionsparametrar ger ett kompakt sätt att definiera datatyper.
- Bättre prestanda: Den kompilatorgenererade likhetsimplementeringen är optimerad och använder inte reflektion som standardimplementeringen.
Använd poster när ditt primära mål är att lagra data och du behöver värdejämlikhetssemantik.
Poster med medlemmar som använder referensjämlikhet
När poster innehåller medlemmar som använder referensjämlikhet fungerar inte det automatiska beteendet för värdejämlikhet för poster som förväntat. Detta gäller för samlingar som System.Collections.Generic.List<T>, matriser och andra referenstyper som inte implementerar värdebaserad likhet (med det anmärkningsvärda undantaget System.String, som implementerar värdejämlikhet).
Viktigt!
Även om poster ger utmärkt värdejämlikhet för grundläggande datatyper löser de inte automatiskt värdejämlikhet för medlemmar som använder referensjämlikhet. Om en post innehåller en System.Collections.Generic.List<T>, System.Array, eller andra referenstyper som inte implementerar värdejämlikhet är två postinstanser med identiskt innehåll i dessa medlemmar fortfarande inte lika eftersom medlemmarna använder referensjämlikhet.
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!
Det beror på att poster använder Object.Equals metoden för varje medlem, och samlingstyper använder vanligtvis referensjämlikhet i stället för att jämföra innehållet.
Följande visar problemet:
// Records with reference-equality members don't work as expected
public record PersonWithHobbies(string Name, List<string> Hobbies);
Så här fungerar detta när du kör koden:
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();
Lösningar för poster med referensjämlikhetsmedlemmar
Anpassad System.IEquatable<T> implementering: Ersätt den kompilatorgenererade likheten med en handkodad version som ger innehållsbaserad jämförelse för referensjämlikhetsmedlemmar. För samlingar implementerar du jämförelse av element för element med hjälp av Enumerable.SequenceEqual eller liknande metoder.
Använd värdetyper där det är möjligt: Tänk på om dina data kan representeras med värdetyper eller oföränderliga strukturer som naturligt stöder värdejämlikhet, till exempel System.Numerics.Vector<T> eller Plane.
Använd typer med värdebaserad likhet: För samlingar bör du överväga att använda typer som implementerar värdebaserad likhet eller implementerar anpassade samlingstyper som åsidosätter Object.Equals för att tillhandahålla innehållsbaserad jämförelse, till exempel System.Collections.Immutable.ImmutableArray<T> eller System.Collections.Immutable.ImmutableList<T>.
Utforma med referensjämlikhet i åtanke: Acceptera att vissa medlemmar använder referensjämlikhet och utforma din programlogik i enlighet med detta, vilket säkerställer att du återanvänder samma instanser när likhet är viktigt.
Här är ett exempel på hur du implementerar anpassad likhet för poster med samlingar:
// 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();
}
}
Den här anpassade implementeringen fungerar korrekt:
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();
Samma problem påverkar matriser och andra samlingstyper:
// These also use reference equality - the issue persists
public record PersonWithHobbiesArray(string Name, string[] Hobbies);
public record PersonWithHobbiesImmutable(string Name, IReadOnlyList<string> Hobbies);
Matriser använder också referensjämlikhet och ger samma oväntade resultat:
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();
Även skrivskyddade samlingar uppvisar det här referensjämlikhetsbeteendet:
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();
Den viktigaste insikten är att poster löser problemet med strukturell likhet men inte ändrar det semantiska likhetsbeteendet för de typer som de innehåller.
Klassexempel
I följande exempel visas hur du implementerar värdejämlikhet i en klass (referenstyp). Den här manuella metoden behövs när du inte kan använda poster eller behöver anpassad likhetslogik:
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
*/
I klasser (referenstyper) utför standardimplementeringen av båda Object.Equals(Object) metoderna en jämförelse av referensjämlikhet, inte en värdejämlikhetskontroll. När en implementer åsidosätter den virtuella metoden är syftet att ge den värdejämlikhetssemantik.
Operatorerna == och != kan användas med klasser även om klassen inte överbelastar dem. Standardbeteendet är dock att utföra en referensjämlikhetskontroll. Om du överbelastar Equals metoden i en klass bör du överbelasta operatorerna == och != , men det krävs inte.
Viktigt!
Föregående exempelkod kanske inte hanterar varje arvsscenario som du förväntar dig. Ta följande kod som exempel:
TwoDPoint p1 = new ThreeDPoint(1, 2, 3);
TwoDPoint p2 = new ThreeDPoint(1, 2, 4);
Console.WriteLine(p1.Equals(p2)); // output: True
Den här koden rapporterar att p1 är lika med p2 trots skillnaden i z-värdena. Skillnaden ignoreras eftersom kompilatorn väljer TwoDPoint implementeringen av IEquatable baserat på kompileringstidstypen. Detta är ett grundläggande problem med polymorf likhet i arvshierarkier.
Polymorf likhet
När du implementerar värdejämlikhet i arvshierarkier med klasser kan standardmetoden som visas i klassexemplet leda till felaktigt beteende när objekt används polymorfiskt. Problemet uppstår eftersom System.IEquatable<T> implementeringar väljs baserat på kompileringstidstyp, inte körningstyp.
Problemet med standardimplementeringar
Tänk på det här problematiska scenariot:
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!
Jämförelsen returneras True eftersom kompilatorn väljer TwoDPoint.Equals(TwoDPoint) baserat på den deklarerade typen och ignorerar koordinatskillnaderna Z .
Nyckeln för att korrigera polymorf likhet är att se till att alla likhetsjämförelser använder den virtuella Object.Equals metoden, som kan kontrollera körningstyper och hantera arv korrekt. Detta kan uppnås genom att använda explicit gränssnittsimplementering för System.IEquatable<T> att delegera till den virtuella metoden:
Basklassen visar nyckelmönstren:
// 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);
}
Den härledda klassen utökar korrekt likhetslogik:
// 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);
}
Så här hanterar den här implementeringen de problematiska polymorfa scenarierna:
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();
Implementeringen hanterar också direkttypsjämförelser korrekt:
// 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();
Likhetsimplementeringen fungerar också korrekt med samlingar:
// 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
Föregående kod visar viktiga element för att implementera värdebaserad likhet:
-
Virtuell
Equals(object?)åsidosättning: Huvudlogik för likhet sker i den virtuella Object.Equals metoden, som anropas oavsett kompileringstidstyp. -
Körningstypkontroll: Om du använder
this.GetType() != p.GetType()ser du till att objekt av olika typer aldrig betraktas som lika. - Explicit gränssnittsimplementering: Implementeringen System.IEquatable<T> delegerar till den virtuella metoden, vilket förhindrar problem med val av kompileringstyp.
-
Skyddad virtuell hjälpmetod: Metoden
protected virtual Equals(TwoDPoint? p)tillåter härledda klasser att åsidosätta likhetslogik samtidigt som typsäkerhet upprätthålls.
Använd det här mönstret i sådana här scenarier:
- Du har arvshierarkier där värdejämlikhet är viktigt
- Objekt kan användas polymorfiskt (deklareras som bastyp, instansierad som härledd typ)
- Du behöver referenstyper med semantik för värdejämlikhet
Den bästa metoden är att använda record typer för att implementera värdebaserad likhet. Den här metoden kräver en mer komplex implementering än standardmetoden och kräver noggrann testning av polymorfa scenarier för att säkerställa korrekthet.
Struct-exempel
I följande exempel visas hur du implementerar värdejämlikhet i en struct (värdetyp). Även om structs har standardvärdejämlikhet kan en anpassad implementering förbättra prestanda:
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
*/
}
För structs utför standardimplementeringen av Object.Equals(Object) (som är den åsidosatta versionen i System.ValueType) en värdejämlikhetskontroll genom att använda reflektion för att jämföra värdena för varje fält i typen. Även om den här implementeringen ger korrekta resultat är den relativt långsam jämfört med en anpassad implementering som du skriver specifikt för typen.
När du åsidosätter den virtuella Equals metoden i en struct är syftet att tillhandahålla ett effektivare sätt att utföra värdejämlikhetskontrollen och eventuellt basera jämförelsen på någon delmängd av structens fält eller egenskaper.
Operatorerna == och != kan inte köras på en struct om inte structen uttryckligen överbelastar dem.