Definování rovnosti hodnot pro třídu nebo strukturu (Průvodce programováním v C#)

Záznamy automaticky implementují rovnost hodnot. Zvažte definování namísto recordclass toho, kdy data modelů typů a měla by implementovat rovnost hodnot.

Při definování třídy nebo struktury se rozhodnete, zda má smysl vytvořit vlastní definici rovnosti hodnot (nebo ekvivalence) pro typ. Rovnost hodnot obvykle implementujete, když očekáváte, že do kolekce přidáte objekty typu nebo když jejich primárním účelem je uložit sadu polí nebo vlastností. Definici rovnosti hodnot můžete založit na porovnání všech polí a vlastností v typu nebo můžete definici založit na podmnožině.

V obou případech a v obou třídách a strukturách by vaše implementace měla dodržovat pět záruk ekvivalence (pro následující pravidla předpokládejme, že xy a z nejsou null):

  1. Reflexní vlastnost: x.Equals(x) vrátí true.

  2. Symetrická vlastnost: x.Equals(y) vrátí stejnou hodnotu jako y.Equals(x).

  3. Tranzitivní vlastnost: pokud (x.Equals(y) && y.Equals(z)) vrátí true, pak x.Equals(z) vrátí true.

  4. Následné vyvolání x.Equals(y) vrátí stejnou hodnotu, pokud objekty, na které odkazuje x a y, se nezmění.

  5. Jakákoli hodnota, která není null, se nerovná hodnotě null. Vyvolá však výjimku, x.Equals(y) pokud x je null. Tím se přeruší pravidla 1 nebo 2 v závislosti na argumentu Equals.

Každá struktura, kterou definujete, již má výchozí implementaci rovnosti hodnoty, kterou dědí z System.ValueType přepsání Object.Equals(Object) metody. Tato implementace používá reflexi k prozkoumání všech polí a vlastností v typu. I když tato implementace vytváří správné výsledky, je relativně pomalé v porovnání s vlastní implementací, kterou píšete speciálně pro daný typ.

Podrobnosti implementace rovnosti hodnot se liší pro třídy a struktury. Obě třídy i struktury však vyžadují pro implementaci rovnosti stejný základní postup:

  1. Přepište virtuálníObject.Equals(Object) metodu. Ve většině případů by vaše implementace bool Equals( object obj ) měla pouze zavolat do metody specifické pro Equals typ, která je implementací System.IEquatable<T> rozhraní. (Viz krok 2.)

  2. System.IEquatable<T> Implementujte rozhraní zadáním metody specifické pro typEquals. Tady se provádí skutečné porovnání ekvivalence. Můžete se například rozhodnout definovat rovnost porovnáním pouze jednoho nebo dvou polí ve vašem typu. Nevyvolávejte výjimky z Equals. Třídy, které souvisejí dědičností:

    • Tato metoda by měla zkoumat pouze pole deklarovaná ve třídě. Měla by volat base.Equals zkoumání polí, která jsou v základní třídě. (Nevolejte base.Equals , pokud typ dědí přímo z Object, protože Object implementace Object.Equals(Object) provádí kontrolu rovnosti odkazů.)

    • Dvě proměnné by měly být považovány za stejné, pouze pokud jsou porovnávané typy proměnných za běhu stejné. Také se ujistěte, že IEquatable implementace Equals metody pro typ běhu je použita, pokud jsou typy runtime a kompilace proměnné odlišné. Jednou strategií pro zajištění správného porovnání typů za běhu je implementace IEquatable pouze ve sealed třídách. Další informace najdete v příkladu třídy dále v tomto článku.

  3. Volitelné, ale doporučené: Přetížení == operátorů a !=

  4. Přepsat Object.GetHashCode tak, aby dva objekty, které mají rovnost hodnot, vytvořily stejný hash kód.

  5. Volitelné: Chcete-li podporovat definice pro "větší než" nebo "menší než", implementujte IComparable<T> rozhraní pro váš typ a také přetěžujte <operátory = a >= .

Poznámka:

Záznamy můžete použít k získání sémantiky rovnosti hodnot bez zbytečného často používaného kódu.

Příklad třídy

Následující příklad ukazuje, jak implementovat rovnost hodnot ve třídě (typ odkazu).

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

U tříd (odkazových typů) výchozí implementace obou Object.Equals(Object) metod provádí porovnání rovnosti odkazů, nikoli kontroly rovnosti hodnot. Když implementátor přepíše virtuální metodu, účelem je dát jí sémantiku rovnosti.

Operátory == a != operátory lze použít s třídami, i když je třída nepřetíží. Výchozí chování je však provést kontrolu rovnosti odkazů. Pokud v třídě přetížíte metodu Equals , měli byste přetížit operátory == a != operátory, ale není to nutné.

Důležité

Předchozí ukázkový kód nemusí zpracovávat každý scénář dědičnosti očekávaným způsobem. Uvažujte následující kód:

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

Tento kód hlásí, že p1 se rovná p2 navzdory rozdílu v z hodnotách. Rozdíl je ignorován, protože kompilátor vybere TwoDPoint implementaci IEquatable na základě typu kompilace.

Předdefinovaná rovnost hodnot typů record zpracovává podobné scénáře správně. Pokud TwoDPoint a ThreeDPoint byly record typy, výsledek p1.Equals(p2) by byl False. Další informace naleznete v tématu Rovnost v record hierarchiích dědičnosti typů.

Příklad struktury

Následující příklad ukazuje, jak implementovat rovnost hodnot ve struktuře (typ hodnoty):

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

U struktur provádí výchozí implementace Object.Equals(Object) (což je přepsáná verze) System.ValueTypekontrolu rovnosti hodnot pomocí reflexe k porovnání hodnot každého pole v typu. Pokud implementátor přepíše virtuální Equals metodu ve struktuře, je účelem poskytnout efektivnější způsob provádění kontroly rovnosti hodnot a volitelně založit porovnání na některých podmnožinách polí nebo vlastností struktury.

Operátory == a != nemohou pracovat se strukturou, pokud je struktura explicitně nepřetěžuje.

Viz také