Condividi tramite


Come definire l'uguaglianza di valori per una classe o un struct (Guida per programmatori C#)

Record implementa automaticamente l'uguaglianza dei valori. Prendere in considerazione la definizione di un record invece di un class quando i dati del tipo modellano e devono implementare l'uguaglianza dei valori.

Quando si definisce una classe o uno struct, si decide se è opportuno creare una definizione personalizzata di uguaglianza di valore, o equivalenza, per il tipo. In genere, l'uguaglianza di valori viene implementata quando si prevede di aggiungere oggetti del tipo a una raccolta, o quando lo scopo principale consiste nell'archiviare un set di campi o di proprietà. È possibile basare la definizione di uguaglianza di valori su un confronto di tutti i campi e di tutte le proprietà nel tipo, oppure su un sottoinsieme.

In entrambi i casi e in entrambe le classi e gli struct, l'implementazione deve seguire le cinque garanzie di equivalenza (per le regole seguenti, si supponga che x, y e z non siano Null):

  1. Proprietà riflessiva: x.Equals(x) restituisce true.

  2. Proprietà simmetrica: x.Equals(y) restituisce lo stesso valore di y.Equals(x).

  3. Proprietà transitiva: se (x.Equals(y) && y.Equals(z)) restituisce true, x.Equals(z) restituisce true.

  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. Qualsiasi valore non Null non è uguale a Null. Tuttavia, x.Equals(y) genera un'eccezione quando x è Null. Che viola le regole 1 o 2, a seconda dell'argomento da Equals.

Gli struct definiti hanno già un'implementazione predefinita di uguaglianza di valore che eredita dall'override System.ValueType del metodo Object.Equals(Object). Questa implementazione usa il processo di reflection per esaminare tutti i campi e tutte le proprietà nel tipo. Sebbene questa implementazione produca risultati corretti, è relativamente lenta rispetto a un'implementazione personalizzata che viene scritta specificamente per il tipo.

I dettagli di implementazione per l'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 di tipo virtual Object.Equals(Object). Nella maggior parte dei casi l'implementazione di bool Equals( object obj ) deve solo chiamare il metodo Equals specifico per il tipo, che è l'implementazione dell'interfaccia System.IEquatable<T>. (Vedere il passaggio 2.)

  2. Implementare l'interfaccia System.IEquatable<T> definendo un metodo Equals specifico per il tipo. È in questo passaggio che viene eseguito il confronto di equivalenza effettivo. Ad esempio, è possibile definire l'uguaglianza confrontando solo uno o due campi nel tipo. Non generare eccezioni da Equals. Per le classi correlate dall'ereditarietà:

    • questo metodo deve esaminare solo i campi che vengono dichiarati nella classe. Deve chiamare base.Equals per esaminare i campi presenti nella classe di base. Non chiamare base.Equals se il tipo eredita direttamente da Object, perché l'implementazione Object di Object.Equals(Object) esegue un controllo di uguaglianza dei riferimenti.

    • Due variabili devono essere considerate uguali solo se i tipi in fase di esecuzione delle variabili confrontate sono uguali. Assicurarsi inoltre che l'implementazione IEquatable del metodo Equals per il tipo in fase di esecuzione venga usata se i tipi in fase di esecuzione e in fase di compilazione di una variabile sono diversi. Una strategia per assicurarsi che i tipi in fase di esecuzione siano sempre confrontati correttamente consiste nell'implementare IEquatable solo nelle classi sealed. Per ulteriori informazioni, vedere l’esempio di classe più avanti in questo articolo.

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

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

  5. Facoltativo: per supportare le definizioni di "maggiore di" o "minore di", implementare l'interfaccia IComparable<T> per il tipo e sottoporre anche a overload gli operatori <= e >=.

Nota

È possibile utilizzare i record per ottenere la semantica dell'uguaglianza dei valori senza alcun codice boilerplate non necessario.

Esempio di classe

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

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

Nelle classi (tipi riferimento) l'implementazione predefinita di entrambi i metodi Object.Equals(Object) esegue un confronto di uguaglianza dei riferimenti, non un controllo di uguaglianza dei valori. Quando un responsabile dell'implementazione esegue l'override del metodo virtuale, lo scopo è assegnare a tale metodo una semantica di uguaglianza di valore.

Gli operatori == e != possono essere usati con le classi anche se la classe non ne esegue l'overload. Tuttavia, il comportamento predefinito è eseguire una verifica dell'uguaglianza dei riferimenti. Se in una classe si esegue l'overload del metodo Equals, è consigliabile eseguire l'overload degli operatori == e !=, ma non è obbligatorio.

Importante

Il codice di esempio precedente potrebbe non gestire tutti gli scenari di ereditarietà nel modo previsto. Osservare il codice seguente:

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

Questo codice segnala che p1 è uguale a p2 nonostante la differenza nei valori z. La differenza viene ignorata perché il compilatore seleziona l'implementazione TwoDPoint di IEquatable in base al tipo in fase di compilazione.

L'uguaglianza dei valori predefiniti dei tipi record gestisce correttamente scenari come questo. Se TwoDPoint e ThreeDPoint fossero tipi record, il risultato di p1.Equals(p2) sarebbe False. Per ulteriori informazioni, consultare Uguaglianza nel tipo record delle gerarchie di ereditarietà.

Esempio di struct

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

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

Per gli struct, l'implementazione predefinita di Object.Equals(Object), che è la versione sottoposta a override in System.ValueType, esegue un controllo di uguaglianza dei valori usando il processo di reflection per confrontare i valori di ogni campo nel tipo. Quando un responsabile dell'implementazione esegue l'override del metodo virtuale Equals in uno struct, lo scopo è specificare un mezzo più efficiente per la verifica dell'uguaglianza di valori e facoltativamente basare il confronto su alcuni subset dei campi o delle proprietà dello struct.

Gli operatori == e != non possono funzionare con uno struct a meno che lo struct non ne esegua esplicitamente l'overload.

Vedi anche