Guide pratique pour définir l’égalité des valeurs pour une classe ou un struct (Guide de programmation C#)

Les enregistrements implémentent automatiquement l’égalité des valeurs. Envisagez de définir un record au lieu d’un class lorsque votre type modélise des données doit implémenter l’égalité des valeurs.

Quand vous définissez une classe ou un struct, vous décidez s’il est judicieux de créer une définition personnalisée de l’égalité des valeurs (ou équivalence) pour le type. En règle générale, vous implémentez l’égalité des valeurs lorsque vous prévoyez d’ajouter des objets de ce type à une collection, ou lorsque leur objectif principal est de stocker un ensemble de champs ou de propriétés. Vous pouvez baser votre définition de l’égalité des valeurs sur une comparaison de tous les champs et propriétés du type, ou vous pouvez la baser sur un sous-ensemble.

Dans les deux cas, et tant dans les classes que dans les structs, votre implémentation doit suivre les cinq garanties d’équivalence (pour les règles suivantes, supposons que x, y et z ne sont pas null) :

  1. La propriété réflexive : x.Equals(x) renvoie true.

  2. La propriété symétrique : x.Equals(y) renvoie la même valeur que y.Equals(x).

  3. La propriété transitive : si (x.Equals(y) && y.Equals(z)) renvoie true, alors x.Equals(z) renvoie true.

  4. Les invocations successives de x.Equals(y) renvoient la même valeur tant que les objets référencés par x et y ne sont pas modifiés.

  5. Toute valeur non null n’est pas égale à null. Toutefois, x.Equals(y) lève une exception lorsque x a la valeur null. Cela enfreint les règles 1 ou 2, selon l’argument de Equals.

Tout struct que vous définissez a déjà une implémentation par défaut de l’égalité des valeurs dont il hérite de la substitution System.ValueType de la méthode Object.Equals(Object). Cette implémentation utilise la réflexion pour examiner tous les champs et propriétés du type. Bien que cette implémentation produise des résultats corrects, elle est relativement lente par rapport à une implémentation personnalisée que vous écrivez spécifiquement pour le type.

Les détails d’implémentation pour l’égalité des valeurs sont différents pour les classes et les structs. Toutefois, les classes et les structs nécessitent tous deux les mêmes étapes de base pour l’implémentation de l’égalité :

  1. Substituez la méthode virtualObject.Equals(Object). Dans la plupart des cas, votre implémentation de bool Equals( object obj ) doit simplement appeler la méthode Equals propre au type qui est l’implémentation de l’interface System.IEquatable<T>. (Voir l’étape 2.)

  2. Implémentez l’interface System.IEquatable<T> en fournissant une méthode Equals propre au type. C’est ici qu’est effectuée la comparaison d’équivalence. Par exemple, vous pouvez décider de définir l’égalité en comparant seulement un ou deux champs de votre type. Ne levez pas d’exceptions à partir de Equals. Pour les classes associées par héritage :

    • cette méthode doit examiner uniquement les champs qui sont déclarés dans la classe. Elle doit appeler base.Equals pour examiner les champs qui sont dans la classe de base. (N’appelez pas base.Equals si le type hérite directement de Object, car Object l’implémentation de Object.Equals(Object) effectue une vérification de l’égalité des références)

    • Deux variables doivent être considérées comme égales uniquement si les types d’exécution des variables comparées sont identiques. Vérifiez également que IEquatablel’implémentation de la Equals méthode pour le type d’exécution est utilisée si les types d’exécution et de compilation d’une variable sont différents. Une stratégie pour s’assurer que les types d’exécution sont toujours comparés correctement consiste à implémenter IEquatable uniquement dans sealedles classes. Pour plus d’informations, consultez l’exemple de classe plus loin dans cet article.

  3. Facultatif, mais recommandé : Surchargez les opérateurs == et ! =.

  4. Substituez Object.GetHashCode pour que deux objets ayant une égalité des valeurs produisent le même code de hachage.

  5. Facultatif : Pour prendre en charge les définitions de « supérieur à » ou « inférieur à », implémentez l’interface IComparable<T> de votre type, et surchargez également les opérateurs <= et >=.

Remarque

Vous pouvez utiliser des enregistrements pour obtenir la sémantique d’égalité des valeurs sans code standard inutile.

Exemple de classe

L’exemple suivant montre comment implémenter l’égalité des valeurs dans une classe (type référence).

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

Sur les classes (types référence), l’implémentation par défaut des deux méthodes Object.Equals(Object) effectue une comparaison d’égalité de référence, et non une vérification de l’égalité des valeurs. Quand un implémenteur substitue la méthode virtuelle, l’objectif est de lui donner une sémantique d’égalité des valeurs.

Les opérateurs == et != peuvent être utilisés avec des classes, même si la classe ne les surcharge pas. Toutefois, le comportement par défaut consiste à effectuer une vérification de l’égalité de référence. Dans une classe, si vous surchargez la méthode Equals, vous devez surcharger les opérateurs == et !=, mais cela n’est pas obligatoire.

Important

L’exemple de code précédent peut ne pas traiter tous les scénarios d’héritage comme vous l’attendez. Prenez le code suivant :

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

Ce code signale que p1 est égal à p2 malgré la différence de valeurs z. La différence est ignorée, car le compilateur choisit l’implémentation TwoDPoint de IEquatable en fonction du type de compilation.

L’égalité de valeur intégrée des record types gère correctement les scénarios de ce type. Si TwoDPoint et ThreeDPoint étaient des types record, le résultat de p1.Equals(p2) serait False. Pour plus d’informations, consultez Égalité dans record les hiérarchies d’héritage de type .

Exemple de struct

L’exemple suivant montre comment implémenter l’égalité des valeurs dans un struct (type valeur) :

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

Pour les structs, l’implémentation par défaut de Object.Equals(Object) (qui est la version substituée dans System.ValueType) effectue une vérification de l’égalité des valeurs à l’aide de la réflexion pour comparer les valeurs de chaque champ dans le type. Quand un implémenteur substitue la méthode virtuelle Equals dans un struct, l’objectif est de fournir un moyen plus efficace d’effectuer la vérification de l’égalité des valeurs, et éventuellement, de baser la comparaison sur un sous-ensemble de champs ou de propriétés du struct.

Les opérateurs == et != ne peuvent pas fonctionner sur un struct, sauf si le struct les surcharge explicitement.

Voir aussi