Dela via


Definiera värdejämlikhet för en klass eller struct (C#-programmeringsguide)

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):

  1. Den reflexiva egenskapen: x.Equals(x) returnerar true.

  2. Den symmetriska egenskapen: x.Equals(y) returnerar samma värde som y.Equals(x).

  3. Den transitiva egenskapen: om (x.Equals(y) && y.Equals(z)) returnerar truereturnerar x.Equals(z).true

  4. Efterföljande anrop av x.Equals(y) returnerar samma värde så länge objekten som refereras av x och y inte ändras.

  5. Alla värden som inte är null är inte lika med null. Genererar dock x.Equals(y) ett undantag när x är null. Det bryter mot regler 1 eller 2, beroende på argumentet till Equals.

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:

  1. Å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 object referenser. Det säkerställer korrekt beteende i samlingar och när du använder polymorfism. I de flesta fall bör implementeringen av bool Equals( object obj ) bara anropa den typspecifika Equals metod som är implementeringen av System.IEquatable<T> gränssnittet. (Se steg 2.)

  2. System.IEquatable<T> Implementera gränssnittet genom att ange en typspecifik Equals metod. 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ån Equals. 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.Equals för att undersöka fält som finns i basklassen. (Anropa base.Equals inte 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 IEquatable av metoden Equals fö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 implementera IEquatable endast i sealed-klasser. Mer information finns i klassexemplet senare i den här artikeln.

  3. 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 == obj2 och obj1.Equals(obj2) beter sig på samma sätt.

  4. Å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> och HashSet<T>. Objekt som är lika med måste ha lika med hashkoder, annars fungerar inte dessa samlingar korrekt.

  5. 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.

Se även