Partager via


Comment : définir une égalité de valeurs pour un type (Guide de programmation C#)

Lorsque vous définissez une classe ou un struct, vous décidez s'il est utile de créer une définition personnalisée de l'égalité (ou équivalence) des valeurs pour le type. En général, vous implémentez l'égalité des valeurs lorsque les objets du type sont supposés être ajoutés à une collection quelconque ou visent principalement à 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 bien sur un sous-ensemble. Toutefois, dans les deux cas, votre implémentation doit respecter les cinq garanties d'équivalence, à la fois dans les classes et les structs :

  1. x.Equals(x) retourne true. Il s'agit de la propriété réflexive.

  2. x.Equals(y) retourne la même valeur que y.Equals(x). Il s'agit de la propriété symétrique.

  3. Si (x.Equals(y) && y.Equals(z)) retourne la valeur true, x.Equals(z) retourne la valeur true. Il s'agit de la propriété transitive.

  4. Des appels successifs de x.Equals(y) retournent la même valeur tant que les objets référencés par x et y ne sont pas modifiés.

  5. x.Equals(null) retourne la valeur false. Toutefois, la comparaison null.Equals (null) lève une exception ; elle ne respecte pas la deuxième règle présentée ci-dessus.

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

Les détails de l'implémentation de l'égalité des valeurs sont différents pour les classes et les structs. Toutefois, les classes et les structs requièrent les mêmes étapes de base pour l'implémentation de l'égalité :

  1. Substituez la méthode virtual Object.Equals(Object). Dans la plupart des cas, votre implémentation de bool Equals( object obj ) doit uniquement appeler la méthode Equals spécifique au type qui correspond à l'implémentation de l'interface IEquatable. (Voir l'étape 2.)

  2. Implémentez l'interface IEquatable en fournissant une méthode Equals spécifique au type. C'est à ce stade que la comparaison d'équivalence est réellement effectuée. 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 la méthode Equals. Pour les classes uniquement : cette méthode doit examiner uniquement les champs déclarés dans la classe. Elle doit appeler base.Equals pour examiner des champs qui figurent dans la classe de base. (N'effectuez pas cette opération si le type hérite directement de Object car l'implémentation Object de Object.Equals(Object) effectue une vérification de l'égalité des références.)

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

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

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

Le premier exemple suivant présente une implémentation de classe. Le deuxième exemple présente une implémentation de struct.

Exemple

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


    namespace ValueEquality
    {
        using System;
        class TwoDPoint : IEquatable<TwoDPoint>
        {
            // Readonly auto-implemented properties. 
            public int X { get; private set; }
            public int Y { get; private set; }

            // Set the properties in the constructor. 
            public TwoDPoint(int x, int y)
            {
                if ((x < 1) || (x > 2000) || (y < 1) || (y > 2000))
                    throw new System.ArgumentException("Point must be in range 1 - 2000");
                this.X = x;
                this.Y = y;
            }

            public override bool Equals(object obj)
            {
                return this.Equals(obj as TwoDPoint);
            }

            public bool Equals(TwoDPoint p)
            {
                // If parameter is null, return false. 
                if (Object.ReferenceEquals(p, 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()
            {
                return X * 0x00010000 + Y;
            }

            public static bool operator ==(TwoDPoint lhs, TwoDPoint rhs)
            {
                // Check for null on left side. 
                if (Object.ReferenceEquals(lhs, null))
                {
                    if (Object.ReferenceEquals(rhs, null))
                    {
                        // null == null = true. 
                        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)
            {
                return !(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 System.ArgumentException("Point must be in range 1 - 2000");
                this.Z = z;
            }

            public override bool Equals(object obj)
            {
                return this.Equals(obj as ThreeDPoint);
            }

            public bool Equals(ThreeDPoint p)
            {
                // If parameter is null, return false. 
                if (Object.ReferenceEquals(p, 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()
            {
                return (X * 0x100000) + (Y * 0x1000) + Z;
            }

            public static bool operator ==(ThreeDPoint lhs, ThreeDPoint rhs)
            {
                // Check for null. 
                if (Object.ReferenceEquals(lhs, null))
                {
                    if (Object.ReferenceEquals(rhs, 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)
            {
                return !(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.
                System.Console.WriteLine("Press any key to exit.");
                System.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é des références, et non pas une vérification de l'égalité des valeurs. Lorsqu'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 une classe même si celle-ci ne les surcharge pas. Toutefois, le comportement par défaut consiste à exécuter une vérification de l'égalité des références. Dans une classe, si vous surchargez la méthode Equals, vous devez également surcharger les opérateurs == et != ; toutefois, cette opération n'est pas obligatoire.

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

    struct TwoDPoint : IEquatable<TwoDPoint>
    {
        // Read/write auto-implemented properties. 
        public int X { get; private set; }
        public int Y { get; private set; }

        public TwoDPoint(int x, int y)
            : this()
        {
            X = x;
            Y = x;
        }

        public override bool Equals(object obj)
        {
            if (obj is TwoDPoint)
            {
                return this.Equals((TwoDPoint)obj);
            }
            return false;
        }

        public bool Equals(TwoDPoint p)
        {
            return (X == p.X) && (Y == p.Y);
        }

        public override int GetHashCode()
        {
            return X ^ Y;
        }

        public static bool operator ==(TwoDPoint lhs, TwoDPoint rhs)
        {
            return lhs.Equals(rhs);
        }

        public static bool operator !=(TwoDPoint lhs, TwoDPoint rhs)
        {
            return !(lhs.Equals(rhs));
        }
    }


    class Program
    {
        static void Main(string[] args)
        {
            TwoDPoint pointA = new TwoDPoint(3, 4);
            TwoDPoint pointB = new TwoDPoint(3, 4);
            int i = 5;

            // Compare using virtual Equals, static Equals, and == and != operators. 
            // 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("pointE.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);

            // Keep the console window open in debug mode.
            System.Console.WriteLine("Press any key to exit.");
            System.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 ValueType) exécute une vérification de l'égalité des valeurs en utilisant la réflexion pour comparer les valeurs de chaque champ du type. Lorsqu'un implémenteur substitue la méthode Equals virtuelle dans un struct, l'objectif est de fournir un moyen plus efficace pour la vérification de l'égalité des valeurs et de baser éventuellement la comparaison sur un sous-ensemble du champ ou des propriétés du struct.

Les opérateurs == et != ne peuvent pas s'appliquer à un struct à moins que celui-ci ne les surcharge explicitement.

Voir aussi

Concepts

Guide de programmation C#

Autres ressources

Comparaisons d'égalité (Guide de programmation C#)