Procedura: definire l'uguaglianza di valori per un tipo (Guida per programmatori C#)

Quando si definisce una classe o uno struct, si stabilisce se abbia senso creare una definizione personalizzata di uguaglianza (o equivalenza) di valori per il tipo. L'uguaglianza di valori viene normalmente implementata quando si prevede che gli oggetti del tipo vengano aggiunti a un insieme di qualche genere o quando lo scopo principale è quello di archiviare un set di campi o di proprietà. È possibile basare la definizione di uguaglianza di valori su un confronto di tutti i campi e tutte le proprietà nel tipo oppure su un sottoinsieme. Ma in entrambi i casi, e sia nelle classi che negli struct, l'implementazione deve seguire le cinque garanzie di equivalenza:

  1. x.Equals(x) restituisce true. Si tratta della proprietà riflessiva.

  2. x.Equals(y) restituisce lo stesso valore di y.Equals(x). Si tratta della proprietà simmetrica.

  3. Se (x.Equals(y) && y.Equals(z)) restituisce true, anche x.Equals(z) restituisce true. Si tratta della proprietà transitiva.

  4. Le successive chiamate di x.Equals(y) restituiscono lo stesso valore purché gli oggetti a cui x e y fanno riferimento non vengano modificati.

  5. x.Equals(null) restituisce false. Tuttavia, null.Equals(null) genera un'eccezione. Non rispetta la precedente regola numero 2.

Gli struct definiti dispongono già di un'implementazione predefinita di uguaglianza di valori che ereditano dall'override System.ValueType del metodo Object.Equals(Object). Questa implementazione utilizza la reflection per esaminare tutte le proprietà e i campi pubblici e non pubblici nel tipo. Anche se questa implementazione produce risultati corretti, è relativamente lenta rispetto a un'implementazione personalizzata scritta specificamente per il tipo.

I dettagli di implementazione relativi all'uguaglianza di valori sono diversi per le classi e gli struct. Tuttavia, sia le classi che gli struct richiedono gli stessi passaggi di base per l'implementazione dell'uguaglianza:

  1. Eseguire l'override del metodo Object.Equals(Object) virtuale. Nella maggior parte dei casi, l'implementazione di bool Equals( object obj ) deve solo eseguire la chiamata nel metodo Equals specifico del tipo che corrisponde all'implementazione dell'interfaccia System.IEquatable<T>. Vedere il passaggio 2.

  2. Implementare l'interfaccia System.IEquatable<T> fornendo un metodo Equals specifico del tipo. Questo è il punto in cui viene eseguito il confronto di equivalenze effettivo. È possibile, ad esempio, decidere di definire l'uguaglianza confrontando solo uno o due campi nel tipo. Non generare eccezioni da Equals. Solo per le classi: questo metodo deve esaminare solo i campi dichiarati nella classe. Deve chiamare base.Equals per esaminare i campi presenti nella classe di base. Non eseguire questa operazione se il tipo eredita direttamente da Object, perché l'implementazione Object di Object.Equals(Object) esegue un controllo dell'uguaglianza dei riferimenti.

  3. Facoltativo, ma consigliato: eseguire l'overload degli operatori == e !=.

  4. Eseguire l'override di Object.GetHashCode in modo che due oggetti che presentano un'uguaglianza di valori producano lo stesso codice hash.

  5. Facoltativo: per supportare le definizioni relative a "maggiore di" o "minore di", implementare l'interfaccia IComparable<T> per il tipo ed eseguire inoltre l'overload degli operatori <= e >=.

Nel primo degli esempi seguenti viene illustrata l'implementazione di una classe. Nel secondo esempio viene illustrata l'implementazione di uno struct.

Esempio

Nell'esempio seguente viene illustrato come implementare l'uguaglianza di valori in una classe (tipo di riferimento).



    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(lhs, 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
        */
    }

Nelle classi (tipi di riferimento), l'implementazione predefinita di entrambi i metodi Object.Equals(Object) esegue un confronto di uguaglianza dei riferimenti, non un controllo dell'uguaglianza dei valori. Un responsabile dell'implementazione esegue l'override del metodo virtuale allo scopo di assegnargli la semantica di uguaglianza dei valori.

Gli operatori == e != possono essere utilizzati con le classi anche se la classe non ne esegue l'overload. Il comportamento predefinito, tuttavia, prevede l'esecuzione di un controllo dell'uguaglianza dei riferimenti. In una classe, se si esegue l'overload del metodo Equals, è possibile eseguire l'overload degli operatori == e !=, sebbene tale operazione non sia necessaria.

Nell'esempio seguente viene illustrato come implementare l'uguaglianza di valori in uno struct (tipo di valore).

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

Per gli struct, l'implementazione predefinita di Object.Equals(Object) (la versione sottoposta a override in System.ValueType) esegue un controllo dell'uguaglianza dei valori utilizzando la reflection per confrontare i valori di ogni campo nel tipo. Un responsabile dell'implementazione esegue l'override del metodo Equals virtuale in uno struct allo scopo di fornire un mezzo più efficiente per eseguire il controllo dell'uguaglianza dei valori e facoltativamente basare il confronto su alcuni sottoinsiemi del campo o delle proprietà dello struct.

Gli operatori == e != non possono operare in uno struct a meno che lo struct non ne esegua l'overload esplicito.

Vedere anche

Riferimenti

Linee guida per l'implementazione del metodo Equals e dell'operatore di uguaglianza (==)

Concetti

Guida per programmatori C#

Altre risorse

Confronto di uguaglianze (Guida per programmatori C#)