Freigeben über


Gewusst wie: Definieren von Wertgleichheit für einen Typ (C#-Programmierhandbuch)

Wenn Sie eine Klasse oder eine Struktur definieren, entscheiden Sie, ob es Sinn macht, eine benutzerdefinierte Definition der Wertgleichheit (oder Äquivalenz) für den Typ zu erstellen. In der Regel implementieren Sie Wertgleichheit, wenn Objekte eines Typs in irgendeiner Weise einer Auflistung hinzugefügt werden sollen, oder wenn ihr primärer Zweck darin besteht, eine Reihe von Feldern oder Eigenschaften zu speichern. Für die Definition der Wertgleichheit können Sie einen Vergleich aller Felder und Eigenschaften im Typ oder aber eine Teilmenge zugrunde legen. Aber in jedem Fall sollte Ihre Implementierung sich sowohl bei Klassen als auch bei Strukturen nach den folgenden Grundsätzen richten:

  1. x.Equals(x) gibt true. zurück. Dies wird als reflexive Eigenschaft bezeichnet.

  2. x.Equals(y) gibt den gleichen Wert zurück wie y.Equals(x). Dies wird als symmetrische Eigenschaft bezeichnet.

  3. Wenn (x.Equals(y) && y.Equals(z)) true zurückgibt, dann gibt x.Equals(z) auch true zurück. Dies wird als transitive Eigenschaft bezeichnet.

  4. Aufeinanderfolgende Aufrufe von x.Equals(y) geben den gleichen Wert zurück, vorausgesetzt, die Objekte, auf die x und y verweisen, werden nicht geändert.

  5. x.Equals(null) gibt false zurück. null.Equals(null) löst jedoch eine Ausnahme aus, die zweite oben stehende Regel gilt hier nicht.

Jede Struktur, die Sie definieren, verfügt bereits über eine Standardimplementierung der Wertgleichheit, die von der System.ValueType-Überschreibung der Object.Equals(Object)-Methode geerbt wird. Diese Implementierung prüft mithilfe von Reflektion alle öffentlichen und nicht öffentlichen Felder und Eigenschaften im Typ. Diese Implementierung führt zwar zu richtigen Ergebnissen, ist jedoch im Vergleich zu einer benutzerdefinierten Implementierung, die Sie speziell für den Typ schreiben, relativ langsam.

Die Implementierungsdetails der Wertgleichheit sind für Klassen und Strukturen unterschiedlich. Die grundlegenden Schritte zur Implementierung der Gleichheit sind jedoch für Klassen und Strukturen dieselben:

  1. Überschreiben Sie die virtual Object.Equals(Object)-Methode. In den meisten Fällen sollte die Implementierung von bool Equals( object obj ) nur die typspezifische Equals-Methode aufrufen, bei der es sich um die Implementierung der System.IEquatable<T>-Schnittstelle handelt. (Siehe Schritt 2.)

  2. Implementieren Sie die System.IEquatable<T>-Schnittstelle, indem Sie eine typspezifische Equals-Methode angeben. An diesem Punkt findet der tatsächliche Äquivalenzvergleich statt. Sie können z. B. entscheiden, zur Definition der Gleichheit den Vergleich von lediglich einem oder zwei Feldern in Ihrem Typ zu wählen. Lösen Sie keine Ausnahmen von Equals aus. Nur für Klassen: Diese Methode darf nur Felder prüfen, die in der Klasse deklariert werden. Zur Prüfung von Feldern in der Basisklasse muss base.Equals aufgerufen werden. (Lassen Sie dies weg, wenn der Typ direkt von Object erbt, da die Object-Implementierung von Object.Equals(Object) eine Verweisgleichheitsprüfung ausführt.)

  3. Optional, aber empfohlen: Überladen des ==-Operators und des !=-Operators.

  4. Überschreiben Sie Object.GetHashCode, damit zwei Objekte mit Wertgleichheit den gleichen Hash erzeugen.

  5. Optional: Zur Unterstützung von Definitionen für "größer als" oder "kleiner als" implementieren Sie die IComparable<T>-Schnittstelle für Ihren Typ, und überladen Sie den <=-Operator und den >=-Operator.

Das erste folgende Beispiel zeigt eine Klassenimplementierung. Das zweite folgende Beispiel zeigt eine Strukturimplementierung.

Beispiel

Im folgenden Beispiel wird die Implementierung der Gleichheitsprüfung in einer Klasse (Verweistyp) veranschaulicht.



    namespace ValueEquality
    {
        using System;
        class TwoDPoint : IEquatable<TwoDPoint>
        {
            // Readonly auto-implemented properties.
            public int X { get; private set; }
            public int Y { get; private set; }

            // Set the properties in the constructor.
            public TwoDPoint(int x, int y)
            {
                if ((x < 1) || (x > 2000) || (y < 1) || (y > 2000))
                    throw new System.ArgumentException("Point must be in range 1 - 2000");
                this.X = x;
                this.Y = y;
            }

            public override bool Equals(object obj)
            {
                return this.Equals(obj as TwoDPoint);
            }

            public bool Equals(TwoDPoint p)
            {
                // If parameter is null, return false.
                if (Object.ReferenceEquals(p, 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()
            {
                return X * 0x00010000 + Y;
            }

            public static bool operator ==(TwoDPoint lhs, TwoDPoint rhs)
            {
                // Check for null on left side.
                if (Object.ReferenceEquals(lhs, null))
                {
                    if (Object.ReferenceEquals(rhs, null))
                    {
                        // null == null = true.
                        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)
            {
                return !(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 System.ArgumentException("Point must be in range 1 - 2000");
                this.Z = z;
            }

            public override bool Equals(object obj)
            {
                return this.Equals(obj as ThreeDPoint);
            }

            public bool Equals(ThreeDPoint p)
            {
                // If parameter is null, return false.
                if (Object.ReferenceEquals(p, 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()
            {
                return (X * 0x100000) + (Y * 0x1000) + Z;
            }

            public static bool operator ==(ThreeDPoint lhs, ThreeDPoint rhs)
            {
                // Check for null.
                if (Object.ReferenceEquals(lhs, null))
                {
                    if (Object.ReferenceEquals(lhs, 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)
            {
                return !(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.
                System.Console.WriteLine("Press any key to exit.");
                System.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
        */
    }

Für Klassen (Verweistypen) führt die Standardimplementierung beider Object.Equals(Object)-Methoden eine Verweisgleichheitsprüfung aus, keine Wertgleichheitsprüfung. Wenn ein Ausführender die virtuelle Methode überschreibt, ist der Zweck die Zuweisung von Wertgleichheitssemantik.

Der ==-Operator und der !=-Operator können mit Klassen verwendet werden, auch wenn die Klasse diese nicht überlädt. Das Standardverhalten ist jedoch die Ausführung einer Verweisgleichheitsprüfung. Wenn Sie in einer Klasse die Equals-Methode überladen, wird empfohlen, die Operatoren == und != zu überladen. Dies ist jedoch nicht zwingend erforderlich.

Im folgenden Beispiel wird die Implementierung der Wertgleichheit in einer Struktur (Werttyp) veranschaulicht:

    struct TwoDPoint : IEquatable<TwoDPoint>
    {
        // Read/write auto-implemented properties.
        public int X { get; private set; }
        public int Y { get; private set; }

        public TwoDPoint(int x, int y)
            : this()
        {
            X = x;
            Y = x;
        }

        public override bool Equals(object obj)
        {
            if (obj is TwoDPoint)
            {
                return this.Equals((TwoDPoint)obj);
            }
            return false;
        }

        public bool Equals(TwoDPoint p)
        {
            return (X == p.X) && (Y == p.Y);
        }

        public override int GetHashCode()
        {
            return X ^ Y;
        }

        public static bool operator ==(TwoDPoint lhs, TwoDPoint rhs)
        {
            return lhs.Equals(rhs);
        }

        public static bool operator !=(TwoDPoint lhs, TwoDPoint rhs)
        {
            return !(lhs.Equals(rhs));
        }
    }


    class Program
    {
        static void Main(string[] args)
        {
            TwoDPoint pointA = new TwoDPoint(3, 4);
            TwoDPoint pointB = new TwoDPoint(3, 4);
            int i = 5;

            // Compare using virtual Equals, static Equals, and == and != operators.
            // 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("pointE.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);

            // Keep the console window open in debug mode.
            System.Console.WriteLine("Press any key to exit.");
            System.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) (die überschriebene Version in System.ValueType) eine Wertgleichheitsprüfung durch, wobei die Werte jedes Feldes im Typ mithilfe von Reflektion verglichen werden. Wenn ein Ausführender die virtuelle Equals-Methode in einer Struktur überschreibt, dient dies zur Bereitstellung einer effizienteren Methode zur Ausführung der Wertgleichheitsprüfung und zur Verwendung einer Teilmenge der Felder und Eigenschaften der Struktur für den Vergleich (optional).

Die Operatoren == und != müssen zur Verwendung für eine Struktur explizit überladen werden.

Siehe auch

Referenz

Richtlinien für die Implementierung der Equals-Methode und des Gleichheitsoperators (==)

Konzepte

C#-Programmierhandbuch

Weitere Ressourcen

Übereinstimmungsvergleiche (C#-Programmierhandbuch)