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

Datensätze implementieren automatisch Wertgleichheit. Erwägen Sie, einen record anstelle einer class zu definieren, wenn Ihr Typ Daten modelliert und Wertgleichheit implementieren soll.

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 virtuelle Methode Object.Equals(Object). 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. 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 !=.

  4. Überschreiben Sie Object.GetHashCode, damit zwei Objekte, die Wertgleichheit besitzen, den gleichen Hashcode erzeugen.

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

Hinweis

Sie können Datensätze verwenden, um ohne unnötige Codebausteine eine Semantik mit Wertgleichheit zu erzielen.

Klassenbeispiel

Im folgenden Beispiel wird veranschaulicht, wie Sie Wertgleichheit in einer Klasse (Referenztyp) implementieren.

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) = {0}", pointA.Equals(pointB));
        Console.WriteLine("pointA == pointB = {0}", pointA == pointB);
        Console.WriteLine("null comparison = {0}", pointA.Equals(pointC));
        Console.WriteLine("Compare to some other type = {0}", pointA.Equals(i));

        TwoDPoint pointD = null;
        TwoDPoint pointE = null;

        Console.WriteLine("Two null TwoDPoints are equal: {0}", pointD == pointE);

        pointE = new TwoDPoint(3, 4);
        Console.WriteLine("(pointE == pointA) = {0}", pointE == pointA);
        Console.WriteLine("(pointA == pointE) = {0}", pointA == pointE);
        Console.WriteLine("(pointA != pointE) = {0}", pointA != pointE);

        System.Collections.ArrayList list = new System.Collections.ArrayList();
        list.Add(new ThreeDPoint(3, 4, 5));
        Console.WriteLine("pointE.Equals(list[0]): {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 eine Implementierer die virtuelle Methode überschreibt, dient dies dazu, ihr Wertgleichheitssemantik zu verleihen.

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 z-Werten gleich p2 ist. Der Unterschied wird ignoriert, da der Compiler die TwoDPoint-Implementierung von IEquatable auf Grundlage des Kompilierzeittyps auswählt.

Die integrierte Wertgleichheit von record-Typen verarbeitet solche Szenarien ordnungsgemäß. Wenn TwoDPoint und ThreeDPointrecord-Typen wären, lautete das Ergebnis von p1.Equals(p2)False. Weitere Informationen finden Sie unter Gleichheit in Vererbungshierarchien von record-Typen.

Strukturbeispiel

Im folgenden Beispiel wird veranschaulicht, wie Sie Wertgleichheit in einer Struktur (Werttyp) implementieren:

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) = {0}", pointA.Equals(pointB));
            // True:
            Console.WriteLine("pointA == pointB = {0}", pointA == pointB);
            // True:
            Console.WriteLine("object.Equals(pointA, pointB) = {0}", object.Equals(pointA, pointB));
            // False:
            Console.WriteLine("pointA.Equals(null) = {0}", pointA.Equals(null));
            // False:
            Console.WriteLine("(pointA == null) = {0}", pointA == null);
            // True:
            Console.WriteLine("(pointA != null) = {0}", pointA != null);
            // False:
            Console.WriteLine("pointA.Equals(i) = {0}", pointA.Equals(i));
            // CS0019:
            // Console.WriteLine("pointA == i = {0}", 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]): {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) = {0}", pointA == pointC);
            // True:
            Console.WriteLine("pointC == pointD = {0}", pointC == pointD);

            TwoDPoint temp = new TwoDPoint(3, 4);
            pointC = temp;
            // True:
            Console.WriteLine("pointA == (pointC = 3,4) = {0}", pointA == pointC);

            pointD = temp;
            // True:
            Console.WriteLine("pointD == (pointC = 3,4) = {0}", 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. Wenn ein Implementierer die virtuelle Equals-Methode in einer Struktur überschreibt, dient dies der Bereitstellung effizienterer Mittel für die Ausführung der Wertgleichheitsprüfung und optional dazu, den Vergleich auf einer Teilmenge 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