Freigeben über


Definieren von Wertgleichheit für eine Klasse oder eine Struktur (C#-Programmierleitfaden)

Tipp

Erwägen Sie zuerst die Verwendung von Datensätzen . Datensätze implementieren automatisch die Wertgleichheit mit minimalem Code, wodurch sie den empfohlenen Ansatz für die meisten datentyporientierten Typen darstellen. Wenn Sie eine logik für die Gleichheit von benutzerdefinierten Werten benötigen oder keine Datensätze verwenden können, fahren Sie mit den nachstehenden manuellen Implementierungsschritten fort.

Wenn Sie eine Klasse oder Struktur definieren, entscheiden Sie, ob es sinnvoll ist, eine benutzerdefinierte Definition der Wertgleichheit (oder Äquivalenz) für den Typ zu erstellen. In der Regel implementieren Sie Wertgleichheit, wenn Objekte des Typs zu einer Auflistung hinzugefügt werden sollen oder wenn ihr Hauptzweck im Speichern einer Reihe von Feldern oder Eigenschaften besteht. Sie können die Definition der Wertgleichheit auf einem Vergleich aller Felder und Eigenschaften im Typ oder auf einer Teilmenge aufbauen.

In beiden Fällen und in beiden Klassen und Strukturen sollte Ihre Implementierung den fünf Garantien der Äquivalenz folgen (für die folgenden Regeln wird angenommen, dass x, y und z nicht NULL sind):

  1. Die reflexive Eigenschaft: x.Equals(x) gibt true zurück.

  2. Die symmetrische Eigenschaft: x.Equals(y) gibt denselben Wert wie y.Equals(x) zurück.

  3. Die transitive Eigenschaft: wenn (x.Equals(y) && y.Equals(z))true zurückgibt, gibt x.Equals(z)true zurück.

  4. Aufeinander folgende Aufrufe von x.Equals(y) geben immer denselben Wert zurück, es sei denn, die Objekte, auf die x und y verweisen, werden geändert.

  5. Ein Nicht-NULL-Wert ist nicht gleich NULL. x.Equals(y) löst jedoch eine Ausnahme aus, wenn x NULL ist. Dadurch wird Regel 1 oder 2 gebrochen, abhängig vom Argument für Equals.

Jede Struktur, die Sie definieren, besitzt bereits eine Standardimplementierung der Wertgleichheit, die von der System.ValueType-Außerkraftsetzung der Object.Equals(Object)-Methode geerbt wurde. Diese Implementierung verwendet Reflektion, um alle Felder und Eigenschaften im Typ zu untersuchen. Obwohl diese Implementierung richtige Ergebnisse produziert, ist sie relativ langsam im Vergleich zu einer benutzerdefinierten Implementierung, die Sie speziell für den Typ schreiben.

Die Implementierungsdetails für Wertgleichheit unterscheiden sich für Klassen und Strukturen. Sowohl Klassen als auch Strukturen erfordern dieselben grundlegenden Schritte zur Implementierung der Gleichheit:

  1. Überschreiben Sie die virtuelleObject.Equals(Object) Methode. Dies bietet ein polymorphes Gleichheitsverhalten, sodass Ihre Objekte korrekt verglichen werden können, wenn sie als object Bezüge behandelt werden. Es sorgt für ein ordnungsgemäßes Verhalten in Sammlungen und bei Verwendung von Polymorphismus. In den meisten Fällen sollte Ihre Implementierung von bool Equals( object obj ) nur die typspezifische Equals-Methode aufrufen, die die Implementierung der Schnittstelle System.IEquatable<T> darstellt. (Siehe Schritt 2)

  2. Implementieren Sie die Schnittstelle System.IEquatable<T>, indem Sie eine typspezifische Equals-Methode bereitstellen. Dies bietet eine typsichere Gleichheitsüberprüfung ohne Boxen, was zu einer besseren Leistung führt. Außerdem werden unnötige Umwandlungen vermieden und die Kompilierungszeittypüberprüfung ermöglicht. An dieser Stelle wird der eigentliche Äquivalenzvergleich ausgeführt. Sie könnten Gleichheit z.B. nur über den Vergleich von ein oder zwei Feldern in Ihrem Typ definieren. Lösen Sie keine Ausnahmen aus Equals aus. Für Klassen, die durch Vererbung verwandt sind:

    • Diese Methode sollte nur Felder prüfen, die in der Klasse deklariert sind. Sie sollte base.Equals aufrufen, um Felder in der Basisklasse zu prüfen. (Rufen Sie nicht base.Equals auf, wenn der Typ direkt von Object erbt, da die Object-Implementierung von Object.Equals(Object) eine Überprüfung der Verweisgleichheit durchführt.)

    • Zwei Variablen sollten nur dann als gleich betrachtet werden, wenn die Laufzeittypen der verglichenen Variablen identisch sind. Stellen Sie außerdem sicher, dass die IEquatable-Implementierung der Equals-Methode für den Laufzeittyp verwendet wird, wenn sich die Laufzeit- und Kompilierzeittypen einer Variablen unterscheiden. Eine Strategie, mit der sichergestellt wird, dass Laufzeittypen immer ordnungsgemäß verglichen werden, besteht darin, IEquatable nur in sealed-Klassen zu implementieren. Weitere Informationen finden Sie weiter unten in diesem Artikel im Klassenbeispiel.

  3. Optional, aber empfohlen: Überladen Sie die Operatoren == und !=. Dies bietet eine konsistente und intuitive Syntax für Gleichheitsvergleiche, die den Erwartungen der Benutzer von integrierten Typen entsprechen. Dadurch wird sichergestellt, dass obj1 == obj2 und obj1.Equals(obj2) sich auf die gleiche Weise verhält.

  4. Überschreiben Sie Object.GetHashCode, damit zwei Objekte, die Wertgleichheit besitzen, den gleichen Hashcode erzeugen. Dies ist erforderlich, um das richtige Verhalten in Hash-basierten Sammlungen wie Dictionary<TKey,TValue> und HashSet<T>. Objekte, die gleich sind, müssen gleiche Hashcodes aufweisen, oder diese Auflistungen funktionieren nicht ordnungsgemäß.

  5. Optional: Implementieren Sie zur Unterstützung von Definitionen für „größer als“ oder „kleiner als“ die Schnittstelle IComparable<T> für Ihren Typ, und überladen Sie die Operatoren <= und >=. Dadurch werden Sortiervorgänge ermöglicht und eine vollständige Sortierbeziehung für Ihren Typ bereitgestellt, die beim Hinzufügen von Objekten zu sortierten Auflistungen oder beim Sortieren von Arrays oder Listen hilfreich ist.

Beispiel für Datensatz

Das folgende Beispiel zeigt, wie Datensätze die Wertgleichstellung automatisch mit minimalem Code implementieren. Der erste Datensatz TwoDPoint ist ein einfacher Datensatztyp, der die Wertgleichstellung automatisch implementiert. Der zweite Datensatz ThreeDPoint zeigt, dass Datensätze aus anderen Datensätzen abgeleitet werden können und dennoch ein ordnungsgemäßes Verhalten der Wertgleichstellung beibehalten werden können:

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 }
*/

Datensätze bieten mehrere Vorteile für die Wertgleichstellung:

  • Automatische Implementierung: Datensätze implementieren System.IEquatable<T> und überschreiben Object.Equalsautomatisch , Object.GetHashCodeund die ==/!= Operatoren.
  • Korrektes Vererbungsverhalten: Datensätze implementieren IEquatable<T> virtuelle Methoden, die den Laufzeittyp beider Operanden überprüfen, um das richtige Verhalten in Vererbungshierarchien und polymorphen Szenarien sicherzustellen.
  • Unveränderlichkeit standardmäßig: Datensätze fördern unveränderliches Design, das gut mit der Wertgleichstellungssemantik funktioniert.
  • Präzise Syntax: Positionsparameter bieten eine kompakte Möglichkeit zum Definieren von Datentypen.
  • Bessere Leistung: Die vom Compiler generierte Gleichheitsimplementierung ist optimiert und verwendet keine Spiegelung wie die Standardstrukturimplementierung.

Verwenden Sie Datensätze, wenn Ihr primäres Ziel darin besteht, Daten zu speichern, und Sie benötigen die Semantik der Wertgleichstellung.

Datensätze mit Mitgliedern, die referenzgleiche Elemente verwenden

Wenn Datensätze Elemente enthalten, die die Referenzgleichheit verwenden, funktioniert das verhalten der automatischen Wertgleichstellung von Datensätzen nicht wie erwartet. Dies gilt für Auflistungen wie System.Collections.Generic.List<T>Arrays und andere Verweistypen, die keine wertbasierte Gleichheit implementieren (mit ausnahme von System.String, die die Wertgleichstellung implementiert).

Wichtig

Während Datensätze eine hervorragende Wertgleichheit für grundlegende Datentypen bieten, lösen sie die Wertgleichheit nicht automatisch für Member, die Referenzgleichheit verwenden. Wenn ein Datensatz einen System.Collections.Generic.List<T>, System.Arrayoder andere Verweistypen enthält, die keine Wertgleichheit implementieren, sind zwei Datensatzinstanzen mit identischem Inhalt in diesen Membern immer noch nicht gleich, da die Member die Referenzgleichheit verwenden.

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!

Dies liegt daran, dass Datensätze die Methode der einzelnen Member verwenden, und Sammlungstypen verwenden in der Object.Equals Regel die Referenzgleichheit, anstatt deren Inhalte zu vergleichen.

Im Folgenden wird das Problem gezeigt:

// Records with reference-equality members don't work as expected
public record PersonWithHobbies(string Name, List<string> Hobbies);

Gehen Sie wie folgt vor, wenn Sie den Code ausführen:

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ösungen für Datensätze mit Mitgliedern der Referenzgleichstellung

  • Benutzerdefinierte System.IEquatable<T> Implementierung: Ersetzen Sie die vom Compiler generierte Gleichheit durch eine handcodierte Version, die einen inhaltsbasierten Vergleich für Referenzgleichheitsmember bereitstellt. Implementieren Sie für Sammlungen den Element-nach-Element-Vergleich mit Enumerable.SequenceEqual oder ähnlichen Methoden.

  • Verwenden Sie werttypen, sofern möglich: Überlegen Sie, ob Ihre Daten mit Werttypen oder unveränderlichen Strukturen dargestellt werden können, die die Wertgleichheit natürlich unterstützen, z System.Numerics.Vector<T> . B. oder Plane.

  • Verwenden Sie Typen mit wertbasierter Gleichheit: Für Sammlungen sollten Sie Typen verwenden, die wertbasierte Gleichheit implementieren oder benutzerdefinierte Sammlungstypen implementieren, die außer Object.Equals Kraft setzen, um einen inhaltsbasierten Vergleich bereitzustellen, z System.Collections.Immutable.ImmutableArray<T> . B. oder System.Collections.Immutable.ImmutableList<T>.

  • Design mit Bezugsgleichheit im Auge: Akzeptieren Sie, dass einige Mitglieder Referenzgleichheit verwenden und Ihre Anwendungslogik entsprechend entwerfen, um sicherzustellen, dass Sie dieselben Instanzen wiederverwenden, wenn die Gleichheit wichtig ist.

Hier ist ein Beispiel für die Implementierung der benutzerdefinierten Gleichheit für Datensätze mit Auflistungen:

// 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();
    }
}

Diese benutzerdefinierte Implementierung funktioniert ordnungsgemäß:

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();

Dasselbe Problem betrifft Arrays und andere Sammlungstypen:

// These also use reference equality - the issue persists
public record PersonWithHobbiesArray(string Name, string[] Hobbies);

public record PersonWithHobbiesImmutable(string Name, IReadOnlyList<string> Hobbies);

Arrays verwenden auch die Referenzgleichheit und erzeugen die gleichen unerwarteten Ergebnisse:

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();

Sogar schreibgeschützte Sammlungen weisen dieses Verhalten der Referenzgleichheit auf:

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();

Der wichtigste Einblick besteht darin, dass Datensätze das Strukturelle Gleichheitsproblem lösen, aber das Verhalten der semantischen Gleichheit der darin enthaltenen Typen nicht ändern.

Klassenbeispiel

Im folgenden Beispiel wird veranschaulicht, wie Sie Wertgleichheit in einer Klasse (Referenztyp) implementieren. Dieser manuelle Ansatz ist erforderlich, wenn Sie keine Datensätze verwenden oder benutzerdefinierte Gleichheitslogik benötigen:

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
*/

Die standardmäßige Implementierung beider Object.Equals(Object)-Methoden führt bei Klassen (Referenztypen) einen Verweisgleichheitsvergleich, keine Wertgleichheitsprüfung aus. Wenn ein Implementierer die virtuelle Methode überschreibt, geschieht dies, damit sie die Semantik der Wertgleichheit erhält.

Die Operatoren == und != können mit Klassen verwendet werden, selbst wenn die Klasse sie nicht überlädt. Allerdings ist das Standardverhalten eine Ausführung einer Verweisgleichheitsprüfung. Wenn Sie die Equals-Methode in einer Klasse überladen, sollten Sie die Operatoren == und != überladen, obwohl dies nicht erforderlich ist.

Wichtig

Der vorherige Beispielcode verarbeitet möglicherweise nicht jedes Vererbungsszenario, wie Sie es erwarten. Betrachten Sie folgenden Code:

TwoDPoint p1 = new ThreeDPoint(1, 2, 3);
TwoDPoint p2 = new ThreeDPoint(1, 2, 4);
Console.WriteLine(p1.Equals(p2)); // output: True

Mit diesem Code wird gemeldet, dass p1 trotz des Unterschieds bei den p2-Werten gleich z ist. Der Unterschied wird ignoriert, da der Compiler die TwoDPoint-Implementierung von IEquatable auf Grundlage des Kompilierzeittyps auswählt. Dies ist ein grundlegendes Problem mit polymorpher Gleichheit in Vererbungshierarchien.

Polymorphe Gleichheit

Bei der Implementierung der Wertgleichstellung in Vererbungshierarchien mit Klassen kann der im Klassenbeispiel gezeigte Standardansatz zu einem falschen Verhalten führen, wenn Objekte polymorph verwendet werden. Das Problem tritt auf, da System.IEquatable<T> Implementierungen basierend auf dem Kompilierungszeittyp und nicht auf laufzeitbasiertem Typ ausgewählt werden.

Das Problem mit Standardimplementierungen

Betrachten Sie dieses problematische Szenario:

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!

Der Vergleich wird zurückgegeben True , da der Compiler basierend auf dem deklarierten TwoDPoint.Equals(TwoDPoint) Typ die Koordinatenunterschiede ignoriert Z .

Der Schlüssel zur Korrektur der polymorphen Gleichheit ist die Sicherstellung, dass alle Gleichheitsvergleiche die virtuelle Object.Equals Methode verwenden, die Laufzeittypen überprüfen und die Vererbung korrekt behandeln kann. Dies kann mithilfe der expliziten Schnittstellenimplementierung für System.IEquatable<T> diese Stellvertretungen an die virtuelle Methode erreicht werden:

Die Basisklasse veranschaulicht die wichtigsten Muster:

// 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);
}

Die abgeleitete Klasse erweitert die Gleichheitslogik ordnungsgemäß:

// 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);
}

So behandelt diese Implementierung die problematischen polymorphen Szenarien:

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();

Die Implementierung behandelt auch die direkten Typvergleiche ordnungsgemäß:

// 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();

Die Implementierung der Gleichheit funktioniert auch ordnungsgemäß mit Sammlungen:

// 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

Der vorangehende Code veranschaulicht die wichtigsten Elemente für die Implementierung wertbasierter Gleichheit:

  • Virtuelle Equals(object?) Außerkraftsetzung: Die Hauptgleichheitslogik erfolgt in der virtuellen Object.Equals Methode, die unabhängig vom Kompilierungstyp aufgerufen wird.
  • Laufzeittypüberprüfung: Durch die Verwendung this.GetType() != p.GetType() wird sichergestellt, dass Objekte unterschiedlicher Typen niemals gleich sind.
  • Explizite Schnittstellenimplementierung: Die System.IEquatable<T> Implementierung delegiert an die virtuelle Methode, um Probleme bei der Auswahl des Kompilierungszeittyps zu verhindern.
  • Geschützte virtuelle Hilfsmethode: Mit der protected virtual Equals(TwoDPoint? p) Methode können abgeleitete Klassen die Gleichheitslogik außer Kraft setzen und gleichzeitig die Typsicherheit beibehalten.

Verwenden Sie dieses Muster in folgenden Fällen:

  • Sie haben Vererbungshierarchien, bei denen die Wertgleichstellung wichtig ist
  • Objekte können polymorph verwendet werden (als Basistyp deklariert, instanziiert als abgeleiteter Typ)
  • Sie benötigen Referenztypen mit Der Semantik der Wertgleichstellung

Der bevorzugte Ansatz besteht darin, Typen zur Implementierung wertbasierter Gleichheit zu verwenden record . Dieser Ansatz erfordert eine komplexere Implementierung als der Standardansatz und erfordert gründliche Tests polymorpher Szenarien, um die Korrektheit sicherzustellen.

Strukturbeispiel

Das folgende Beispiel zeigt, wie Die Wertgleichheit in einer Struktur (Werttyp) implementiert wird. Während Strukturen die Gleichheit von Standardwerten aufweisen, kann eine benutzerdefinierte Implementierung die Leistung verbessern:

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 Strukturen führt die Standardimplementierung von Object.Equals(Object) (was die außer Kraft gesetzte Version in System.ValueType ist) eine Überprüfung der Wertgleichheit durch, indem sie Reflektion zum Vergleichen der Werte jedes Felds im Typ verwendet. Obwohl diese Implementierung richtige Ergebnisse produziert, ist sie relativ langsam im Vergleich zu einer benutzerdefinierten Implementierung, die Sie speziell für den Typ schreiben.

Wenn Sie die virtuelle Equals Methode in einer Struktur außer Kraft setzen, besteht der Zweck darin, eine effizientere Methode zum Ausführen der Wertgleichstellungsprüfung bereitzustellen und optional den Vergleich auf einigen Teilmengen der Felder oder Eigenschaften der Struktur zu basieren.

Die Operatoren == und != können nicht auf Strukturen operieren, es sei denn, die Struktur überlädt sie explizit.

Siehe auch