Jak zdefiniować równość wartości dla klasy lub struktury (Przewodnik programowania w języku C#)

Rekordy automatycznie implementują równość wartości. Rozważ zdefiniowanie wartości record zamiast class wartości, gdy dane modeli typów i powinny implementować równość wartości.

Podczas definiowania klasy lub struktury decydujesz, czy warto utworzyć niestandardową definicję równości wartości (lub równoważności) dla typu. Zazwyczaj należy zaimplementować równość wartości, gdy oczekuje się dodania obiektów typu do kolekcji lub gdy ich głównym celem jest przechowywanie zestawu pól lub właściwości. Definicję równości wartości można opierać na porównywaniu wszystkich pól i właściwości w typie lub można opierać definicję w podzestawie.

W obu przypadkach, a w obu klasach i strukturach implementacja powinna przestrzegać pięciu gwarancji równoważności (w przypadku następujących reguł przyjęto założenie, że xi zy nie ma wartości null):

  1. Właściwość refleksyjna: x.Equals(x) zwraca wartość true.

  2. Właściwość symetryczna: x.Equals(y) zwraca tę samą wartość co y.Equals(x).

  3. Właściwość przechodnia: jeśli (x.Equals(y) && y.Equals(z)) zwraca truewartość , zwraca x.Equals(z) wartość true.

  4. Kolejne wywołania x.Equals(y) zwracają tę samą wartość, o ile obiekty, do których odwołuje się x i y, nie są modyfikowane.

  5. Każda wartość inna niż null nie jest równa null. x.Equals(y) Zgłasza jednak wyjątek, gdy x ma wartość null. To przerywa reguły 1 lub 2, w zależności od argumentu na Equals.

Każda zdefiniowana struktura ma już domyślną implementację równości wartości dziedziczonej System.ValueType po zastąpieniu Object.Equals(Object) metody . Ta implementacja używa odbicia w celu zbadania wszystkich pól i właściwości w typie. Mimo że ta implementacja generuje poprawne wyniki, jest stosunkowo niska w porównaniu z implementacją niestandardową, którą piszesz specjalnie dla tego typu.

Szczegóły implementacji równości wartości różnią się w przypadku klas i struktur. Jednak obie klasy i struktury wymagają tych samych podstawowych kroków implementowania równości:

  1. Zastąpij metodę wirtualnąObject.Equals(Object). W większości przypadków implementacja bool Equals( object obj ) polecenia powinna po prostu wywołać metodę specyficzną dla Equals typu, która jest implementacją interfejsu System.IEquatable<T> . (Zobacz krok 2.)

  2. Zaimplementuj System.IEquatable<T> interfejs, podając metodę specyficzną dla Equals typu. W tym miejscu jest wykonywane rzeczywiste porównanie równoważności. Na przykład możesz zdecydować się na zdefiniowanie równości, porównując tylko jedno lub dwa pola w twoim typie. Nie zgłaszaj wyjątków z elementu Equals. W przypadku klas, które są powiązane przez dziedziczenie:

    • Ta metoda powinna sprawdzać tylko pola zadeklarowane w klasie. Wywołaj metodę base.Equals , aby zbadać pola, które znajdują się w klasie bazowej. (Nie należy wywoływać base.Equals , jeśli typ dziedziczy bezpośrednio z Objectklasy , ponieważ Object implementacja Object.Equals(Object) wykonuje sprawdzanie równości odwołań).

    • Dwie zmienne należy uznać za równe tylko wtedy, gdy porównywane typy zmiennych w czasie wykonywania są takie same. Upewnij się również, że IEquatable implementacja Equals metody dla typu czasu wykonywania jest używana, jeśli typy czasu wykonywania i kompilowania zmiennej są inne. Jedną ze strategii upewnienia się, że typy czasu wykonywania są zawsze porównywane poprawnie, jest zaimplementowanie IEquatable tylko w sealed klasach. Aby uzyskać więcej informacji, zobacz przykład klasy w dalszej części tego artykułu.

  3. Opcjonalne, ale zalecane: Przeciążać == operatory i != .

  4. Zastąpij Object.GetHashCode tak, aby dwa obiekty, które mają równość wartości, tworzyły ten sam kod skrótu.

  5. Opcjonalnie: Aby obsługiwać definicje "większe niż" lub "mniejsze niż", zaimplementuj IComparable<T> interfejs dla danego typu, a także przeciąż <operatory = i >= .

Uwaga

Za pomocą rekordów można uzyskać semantyka równości wartości bez niepotrzebnego kodu kociołowego.

Przykład klasy

W poniższym przykładzie pokazano, jak zaimplementować równość wartości w klasie (typ odwołania).

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

W przypadku klas (typów referencyjnych) domyślna implementacja obu Object.Equals(Object) metod wykonuje porównanie równości odwołań, a nie sprawdzanie równości wartości. Gdy implementator zastępuje metodę wirtualną, celem jest nadanie jej semantyki równości.

Operatory == i != mogą być używane z klasami, nawet jeśli klasa ich nie przeciąża. Jednak domyślne zachowanie polega na wykonaniu sprawdzania równości odwołania. W klasie, jeśli przeciążysz metodę Equals , należy przeciążyć == operatory i != , ale nie jest to wymagane.

Ważne

Powyższy przykładowy kod może nie obsługiwać każdego scenariusza dziedziczenia w oczekiwany sposób. Spójrzmy na poniższy kod:

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

Ten kod raportuje, że p1 jest p2 równa niezależnie od różnicy w z wartościach. Różnica jest ignorowana, ponieważ kompilator wybiera implementację TwoDPointIEquatable na podstawie typu czasu kompilacji.

Wbudowana równość record wartości typów obsługuje takie scenariusze, jak to poprawnie. Gdyby TwoDPoint typy i ThreeDPoint były record , wynikiem polecenia p1.Equals(p2) będzie False. Aby uzyskać więcej informacji, zobacz Równość w record hierarchii dziedziczenia typów.

Przykład struktury

W poniższym przykładzie pokazano, jak zaimplementować równość wartości w strukturach (typ wartości):

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

W przypadku struktur domyślna implementacja Object.Equals(Object) (która jest zastępowaną wersją w systemie System.ValueType) wykonuje sprawdzanie równości wartości przy użyciu odbicia w celu porównania wartości każdego pola w typie. Gdy implementator zastępuje metodę wirtualną Equals w strukturę, celem jest zapewnienie bardziej wydajnego sposobu przeprowadzania sprawdzania równości wartości i opcjonalnie w celu oparcia porównania na niektórych podzestawach pól lub właściwości struktury.

Operatory == i != nie mogą działać na strukturę, chyba że struktura jawnie je przeciąża.

Zobacz też