Share via


Waarde-gelijkheid definiëren voor een klasse of struct (C#-programmeerhandleiding)

Records implementeren automatisch gelijkheid van waarden. Overweeg om een record in plaats van een class wanneer uw type gegevens modelleert en waarde gelijkheid moet implementeren.

Wanneer u een klasse of struct definieert, bepaalt u of het zinvol is om een aangepaste definitie van waarde gelijkheid (of gelijkwaardigheid) te maken voor het type. Normaal gesproken implementeert u gelijkheid van waarden wanneer u verwacht objecten van het type aan een verzameling toe te voegen of wanneer het primaire doel is om een set velden of eigenschappen op te slaan. U kunt uw definitie van waarde gelijkheid baseren op een vergelijking van alle velden en eigenschappen in het type, of u kunt de definitie baseren op een subset.

In beide gevallen en in beide klassen en structs moet uw implementatie de vijf garanties van gelijkwaardigheid volgen (voor de volgende regels wordt ervan uitgegaan dat xen y z niet null zijn):

  1. De reflexieve eigenschap: x.Equals(x) retourneert true.

  2. De symmetrische eigenschap: x.Equals(y) retourneert dezelfde waarde als y.Equals(x).

  3. De transitieve eigenschap: als (x.Equals(y) && y.Equals(z)) retourneert true, retourneert x.Equals(z) true.

  4. Opeenvolgende aanroepen van x.Equals(y) retourneren dezelfde waarde zolang de objecten waarnaar wordt verwezen door x en y niet worden gewijzigd.

  5. Een niet-null-waarde is niet gelijk aan null. x.Equals(y) Genereert echter een uitzondering wanneer x null is. Dat breekt regels 1 of 2, afhankelijk van het argument aan Equals.

Elke struct die u definieert, heeft al een standaard implementatie van waarde gelijkheid die wordt overgenomen van de System.ValueType onderdrukking van de Object.Equals(Object) methode. Deze implementatie maakt gebruik van weerspiegeling om alle velden en eigenschappen in het type te onderzoeken. Hoewel deze implementatie correcte resultaten oplevert, is het relatief traag vergeleken met een aangepaste implementatie die u specifiek schrijft voor het type.

De implementatiedetails voor waarde gelijkheid verschillen voor klassen en structs. Voor zowel klassen als structs zijn echter dezelfde basisstappen vereist voor het implementeren van gelijkheid:

  1. De virtuele Object.Equals(Object) methode overschrijven. In de meeste gevallen moet uw implementatie bool Equals( object obj ) alleen de typespecifieke Equals methode aanroepen die de implementatie van de System.IEquatable<T> interface is. (Zie stap 2.)

  2. Implementeer de System.IEquatable<T> interface door een typespecifieke Equals methode te bieden. Hier wordt de daadwerkelijke gelijkwaardigheidsvergelijking uitgevoerd. U kunt bijvoorbeeld besluiten gelijkheid te definiëren door slechts één of twee velden in uw type te vergelijken. Gooi geen uitzonderingen van Equals. Voor klassen die zijn gerelateerd aan overname:

    • Met deze methode moeten alleen velden worden onderzocht die in de klasse zijn gedeclareerd. Er moet een aanroep base.Equals worden uitgevoerd om velden te onderzoeken die zich in de basisklasse bevinden. (Roep niet base.Equals aan als het type rechtstreeks overgaat van Object, omdat de Object implementatie van Object.Equals(Object) een verwijzing gelijkheidscontrole uitvoert.)

    • Twee variabelen moeten alleen gelijk worden beschouwd als de runtimetypen van de variabelen die worden vergeleken, hetzelfde zijn. Zorg er ook voor dat de implementatie van de IEquatable Equals methode voor het runtimetype wordt gebruikt als de runtime- en compileertijdtypen van een variabele verschillen. Eén strategie om ervoor te zorgen dat runtimetypen altijd correct worden vergeleken, is alleen in sealed klassen te implementerenIEquatable. Zie het voorbeeld van de klas verderop in dit artikel voor meer informatie.

  3. Optioneel maar aanbevolen: de operators en != overbelasten==.

  4. Overschrijven Object.GetHashCode zodat twee objecten met waarde-gelijkheid dezelfde hash-code produceren.

  5. Optioneel: Ter ondersteuning van definities voor 'groter dan' of 'kleiner dan', implementeert u de IComparable<T> interface voor uw type en overbelasting van de <operatoren = en >= .

Notitie

U kunt records gebruiken om semantiek voor waarde gelijkheid op te halen zonder onnodige standaardcode.

Voorbeeld van klasse

In het volgende voorbeeld ziet u hoe u gelijkheid van waarden in een klasse (verwijzingstype) implementeert.

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

Bij klassen (referentietypen) voert de standaard implementatie van beide Object.Equals(Object) methoden een vergelijking van referentie gelijkheid uit, niet een waarde gelijkheidscontrole. Wanneer een implementeerfunctie de virtuele methode overschrijft, is het doel deze semantiek van gelijkheid te geven.

De == operators kunnen != worden gebruikt met klassen, zelfs als de klasse ze niet overbelast. Het standaardgedrag is echter om een referentie-gelijkheidscontrole uit te voeren. Als u de Equals methode in een klasse overbelast, moet u de == en != operators overbelasten, maar dit is niet vereist.

Belangrijk

De voorgaande voorbeeldcode verwerkt mogelijk niet elk overnamescenario zoals u verwacht. Kijk eens naar de volgende code:

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

Deze code rapporteert dat p1 gelijk is p2 aan het verschil in z waarden. Het verschil wordt genegeerd omdat de compiler de TwoDPoint implementatie kiest op basis van IEquatable het type compileertijd.

De ingebouwde waarde gelijkheid van record typen verwerkt scenario's zoals deze correct. Als TwoDPoint en ThreeDPoint typen zijn record , zou het resultaat p1.Equals(p2) zijn False. Zie Gelijkheid in overnamehiërarchieën van record het type voor meer informatie.

Voorbeeld van struct

In het volgende voorbeeld ziet u hoe u gelijkheid van waarden implementeert in een struct (waardetype):

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

Voor structs voert de standaard implementatie van Object.Equals(Object) (de overschreven versie in System.ValueType) een waarde gelijkheidscontrole uit met behulp van weerspiegeling om de waarden van elk veld in het type te vergelijken. Wanneer een implementeerfunctie de virtuele Equals methode in een struct overschrijft, is het doel een efficiëntere manier te bieden om de gelijkheidscontrole van waarden uit te voeren en optioneel de vergelijking te baseren op een deelverzameling van de velden of eigenschappen van de struct.

De == operatoren en != kunnen niet op een struct werken, tenzij de struct deze expliciet overbelast.

Zie ook