Dela via


Definiera värdejämlikhet för en klass eller struct (C#-programmeringsguide)

Poster implementerar automatiskt värdejämlikhet. Överväg att definiera en record i stället för en class när din typ modellerar data och bör implementera värdejämlikhet.

När du definierar en klass eller struct bestämmer du om det är lämpligt att skapa en anpassad definition av värdejämlikhet (eller likvärdighet) för typen. Vanligtvis implementerar du värdejämlikhet när du förväntar dig att lägga till objekt av typen i en samling, eller när deras primära syfte är att lagra en uppsättning fält eller egenskaper. Du kan basera definitionen av värdejämlikhet på en jämförelse av alla fält och egenskaper i typen, eller så kan du basera definitionen på en delmängd.

I båda fallen och i både klasser och structs bör implementeringen följa de fem garantierna för likvärdighet (för följande regler antar du att x, y och z inte är null):

  1. Den reflexiva egenskapen: x.Equals(x) returnerar true.

  2. Den symmetriska egenskapen: x.Equals(y) returnerar samma värde som y.Equals(x).

  3. Den transitiva egenskapen: om (x.Equals(y) && y.Equals(z)) returnerar truereturnerar true.x.Equals(z)

  4. Efterföljande anrop av x.Equals(y) returnerar samma värde så länge objekten som refereras av x och y inte ändras.

  5. Alla värden som inte är null är inte lika med null. Genererar dock x.Equals(y) ett undantag när x är null. Det bryter mot regler 1 eller 2, beroende på argumentet till Equals.

Alla struct som du definierar har redan en standardimplementering av värdejämlikhet som den ärver från åsidosättningen System.ValueType Object.Equals(Object) av metoden. Den här implementeringen använder reflektion för att undersöka alla fält och egenskaper i typen. Även om den här implementeringen ger korrekta resultat är den relativt långsam jämfört med en anpassad implementering som du skriver specifikt för typen.

Implementeringsinformationen för värdejämlikhet skiljer sig åt för klasser och structs. Både klasser och structs kräver dock samma grundläggande steg för att implementera likhet:

  1. Åsidosätt den virtuella Object.Equals(Object) metoden. I de flesta fall bör implementeringen av bool Equals( object obj ) bara anropa den typspecifika Equals metod som är implementeringen av System.IEquatable<T> gränssnittet. (Se steg 2.)

  2. System.IEquatable<T> Implementera gränssnittet genom att ange en typspecifik Equals metod. Det är här som den faktiska jämförelsen av ekvivalens utförs. Du kan till exempel välja att definiera likhet genom att bara jämföra ett eller två fält i din typ. Utlöser inte undantag från Equals. För klasser som är relaterade till arv:

    • Den här metoden bör endast undersöka fält som deklareras i klassen. Den bör anropa base.Equals för att undersöka fält som finns i basklassen. (Anropa base.Equals inte om typen ärver direkt från Object, eftersom implementeringen Object av Object.Equals(Object) utför en referensjämlikhetskontroll.)

    • Två variabler bör endast anses vara lika med om körningstyperna för variablerna som jämförs är desamma. Kontrollera också att implementeringen IEquatable av Equals metoden för körningstypen används om körnings- och kompileringstidstyperna för en variabel skiljer sig. En strategi för att se till att körningstyper alltid jämförs korrekt är att endast implementera IEquatable i sealed klasser. Mer information finns i klassexemplet senare i den här artikeln.

  3. Valfritt men rekommenderas: Överbelasta operatorerna == och != .

  4. Åsidosätt Object.GetHashCode så att två objekt som har värdejämlikhet skapar samma hash-kod.

  5. Valfritt: Om du vill stödja definitioner för "större än" eller "mindre än" implementerar du IComparable<T> gränssnittet för din typ och överbelastar även operatorerna< = och >= .

Kommentar

Du kan använda poster för att hämta värdejämlikhetssemantik utan onödig pannplåtskod.

Klassexempel

I följande exempel visas hur du implementerar värdejämlikhet i en klass (referenstyp).

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

I klasser (referenstyper) utför standardimplementeringen av båda Object.Equals(Object) metoderna en jämförelse av referensjämlikhet, inte en värdejämlikhetskontroll. När en implementer åsidosätter den virtuella metoden är syftet att ge den värdejämlikhetssemantik.

Operatorerna == och != kan användas med klasser även om klassen inte överbelastar dem. Standardbeteendet är dock att utföra en referensjämlikhetskontroll. Om du överbelastar Equals metoden i en klass bör du överbelasta operatorerna == och != , men det krävs inte.

Viktigt!

Föregående exempelkod kanske inte hanterar varje arvsscenario som du förväntar dig. Ta följande kod som exempel:

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

Den här koden rapporterar som p1 är p2 lika med trots skillnaden i z värden. Skillnaden ignoreras eftersom kompilatorn väljer TwoDPoint implementeringen av IEquatable baserat på kompileringstidstypen.

Den inbyggda värdejämlikheten för record typer hanterar scenarier som detta korrekt. Om TwoDPoint och ThreeDPoint var record typer skulle resultatet av p1.Equals(p2) vara False. Mer information finns i Likhet i record typarvhierarkier.

Struct-exempel

I följande exempel visas hur du implementerar värdejämlikhet i en struct (värdetyp):

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 structs utför standardimplementeringen av Object.Equals(Object) (som är den åsidosatta versionen i System.ValueType) en värdejämlikhetskontroll genom att använda reflektion för att jämföra värdena för varje fält i typen. När en implementer åsidosätter den virtuella Equals metoden i en struct är syftet att tillhandahålla ett effektivare sätt att utföra värdejämlikhetskontrollen och eventuellt basera jämförelsen på någon delmängd av structens fält eller egenskaper.

Operatorerna == och != kan inte köras på en struct om inte structen uttryckligen överbelastar dem.

Se även