Operatori di uguaglianza: verificare se due oggetti sono uguali o meno

Gli operatori == (uguaglianza) e != (disuguaglianza) controllano se i relativi operandi sono uguali oppure no. I tipi valore sono uguali quando il contenuto è uguale. I tipi di riferimento sono uguali quando le due variabili fanno riferimento alla stessa risorsa di archiviazione.

Il riferimento al linguaggio C# documenta la versione rilasciata più di recente del linguaggio C#. Contiene anche la documentazione iniziale per le funzionalità nelle versioni di anteprima pubblica per la prossima versione del linguaggio di programmazione.

La documentazione identifica tutte le funzionalità introdotte nelle ultime tre versioni della lingua o nelle anteprime pubbliche correnti.

Suggerimento

Per trovare quando una funzionalità è stata introdotta per la prima volta in C#, vedere l'articolo sulla cronologia delle versioni del linguaggio C#.

È possibile usare l'operatore di corrispondenza dei is criteri come alternativa a un == test quando si esegue il test su un valore costante. L'operatore is usa la semantica di uguaglianza predefinita per tutti i tipi valore e riferimento.

Operatore di uguaglianza ==

L'operatore di uguaglianza == restituisce true se gli operandi sono uguali, false in caso contrario.

Uguaglianza dei tipi valore

Gli operandi dei tipi valore predefiniti sono uguali se i relativi valori sono uguali:

int a = 1 + 2 + 3;
int b = 6;
Console.WriteLine(a == b);  // output: True

char c1 = 'a';
char c2 = 'A';
Console.WriteLine(c1 == c2);  // output: False
Console.WriteLine(c1 == char.ToLower(c2));  // output: True

Nota

Per gli ==operatori , <>, <=, e >= , se uno degli operandi non è un numero (Double.NaN o Single.NaN), il risultato dell'operazione è false. Questa condizione indica che il NaN valore non è maggiore, minore o uguale a qualsiasi altro double valore (o float), incluso NaN. Per altre informazioni ed esempi, vedere l'articolo di riferimento per Double.NaN o Single.NaN.

Due operandi dello stesso tipo enum sono uguali se i valori corrispondenti del tipo integrale sottostante sono uguali.

I tipi struct definiti dall'utente non supportano l'operatore == per impostazione predefinita. Per supportare l'operatore ==, un tipo struct definito dall'utente deve eseguirne l'overload.

Le tuple C# hanno il supporto predefinito per gli == operatori e != . Per altre informazioni, vedere la sezione Uguaglianza delle tuple nell'articolo Tipi di tupla.

Uguaglianza dei tipi riferimento

Per impostazione predefinita, gli operandi di tipo riferimento, esclusi i record, sono uguali se fanno riferimento allo stesso oggetto:

public class ReferenceTypesEquality
{
    public class MyClass
    {
        private int id;

        public MyClass(int id) => this.id = id;
    }

    public static void Main()
    {
        var a = new MyClass(1);
        var b = new MyClass(1);
        var c = a;
        Console.WriteLine(a == b);  // output: False
        Console.WriteLine(a == c);  // output: True
    }
}

Come illustrato nell'esempio precedente, i tipi di riferimento definiti dall'utente supportano l'operatore == per impostazione predefinita. Tuttavia, un tipo riferimento può eseguire l'overload dell'operatore ==. Se un tipo riferimento esegue l'overload dell'operatore ==, usare il metodo Object.ReferenceEquals per verificare se due riferimenti di quel tipo fanno riferimento allo stesso oggetto.

Uguaglianza dei tipi di record

I tipi di record supportano gli operatori == e != che per impostazione predefinita forniscono la semantica di uguaglianza dei valori. Ovvero, due operandi di record sono uguali quando entrambi sono null o i valori corrispondenti di tutti i campi e le proprietà implementate automaticamente sono uguali.

public class RecordTypesEquality
{
    public record Point(int X, int Y, string Name);
    public record TaggedNumber(int Number, List<string> Tags);

    public static void Main()
    {
        var p1 = new Point(2, 3, "A");
        var p2 = new Point(1, 3, "B");
        var p3 = new Point(2, 3, "A");

        Console.WriteLine(p1 == p2);  // output: False
        Console.WriteLine(p1 == p3);  // output: True

        var n1 = new TaggedNumber(2, new List<string>() { "A" });
        var n2 = new TaggedNumber(2, new List<string>() { "A" });
        Console.WriteLine(n1 == n2);  // output: False
    }
}

Come illustrato nell'esempio precedente, l'uguaglianza dei membri di tipo riferimento viene confrontata usando le implementazioni di uguaglianza specifiche.

Uguaglianza di stringhe

Due operandi stringa sono uguali quando entrambi sono null o entrambe le istanze di stringa sono della stessa lunghezza e contengono caratteri identici in ogni posizione di carattere:

string s1 = "hello!";
string s2 = "HeLLo!";
Console.WriteLine(s1 == s2.ToLower());  // output: True

string s3 = "Hello!";
Console.WriteLine(s1 == s3);  // output: False

I confronti di uguaglianza tra stringhe sono confronti ordinali con distinzione tra maiuscole e minuscole. Per altre informazioni sul confronto di stringhe, vedere Come confrontare stringhe in C#.

Delegare l'uguaglianza

Due operandi delegati dello stesso tipo di runtime sono uguali quando entrambi sono null o i relativi elenchi di chiamate hanno la stessa lunghezza e hanno voci uguali in ogni posizione:

Action a = () => Console.WriteLine("a");

Action b = a + a;
Action c = a + a;
Console.WriteLine(object.ReferenceEquals(b, c));  // output: False
Console.WriteLine(b == c);  // output: True

Importante

Le voci uguali in un elenco chiamate includono tutti i parametri fissi nella chiamata, incluso il ricevitore. Il ricevitore è l'istanza di un oggetto rappresentato da this quando viene richiamata la voce.

var o1 = new object();
var o2 = new object();
var d1 = o1.ToString;
var d2 = o2.ToString;
Console.WriteLine(object.ReferenceEquals(d1, d2));  // output: False
Console.WriteLine(d1 == d2);  // output: False (different receivers)

Per altre informazioni, vedere la sezione Delegare gli operatori di uguaglianza dell'articolo Specifiche del linguaggio C#.

I delegati provenienti dalla valutazione di espressioni lambda semanticamente identiche non sono uguali, come illustrato nell'esempio seguente:

Action a = () => Console.WriteLine("a");
Action b = () => Console.WriteLine("a");

Console.WriteLine(a == b);  // output: False
Console.WriteLine(a + b == a + b);  // output: True
Console.WriteLine(b + a == a + b);  // output: False

Operatore di disuguaglianza !=

L'operatore != di disuguaglianza restituisce true se gli operandi non sono uguali e false in caso contrario. Per gli operandi dei tipi predefiniti, l'espressione x != y produce lo stesso risultato dell'espressione !(x == y). Per altre informazioni sull'uguaglianza dei tipi, vedere la sezione Operatore di uguaglianza.

Nell'esempio seguente viene illustrato come usare l'operatore != :

int a = 1 + 1 + 2 + 3;
int b = 6;
Console.WriteLine(a != b);  // output: True

string s1 = "Hello";
string s2 = "Hello";
Console.WriteLine(s1 != s2);  // output: False

object o1 = 1;
object o2 = 1;
Console.WriteLine(o1 != o2);  // output: True

Uguaglianza nelle gerarchie di classi

I record gestiscono correttamente l'ereditarietà senza lavoro manuale. L'uguaglianza generata dal compilatore controlla sia il tipo di runtime che tutte le proprietà dichiarate, quindi soddisfa automaticamente i requisiti di simmetria e transitività. Preferisce record rispetto a una gerarchia manuale non bloccato quando l'uguaglianza dei valori è l'obiettivo.

Importante

Usare record quando possibile: il compilatore genera automaticamente tutti i membri di uguaglianza necessari. L'implementazione manuale è necessaria solo quando il tipo deve derivare da una classe non record o ha altri vincoli che impediscono record.

Implementa manualmente l'uguaglianza quando un tipo non può essere un record

Ecco un'implementazione manuale minima per un tipo di valore che non può essere un record:

class Color : IEquatable<Color>
{
    public Color(int r, int g, int b)
    {
        R = r;
        G = g;
        B = b;
    }

    public int R { get; }
    public int G { get; }
    public int B { get; }

    public bool Equals(Color? other) =>
        other is not null && R == other.R && G == other.G && B == other.B;

    public override bool Equals(object? obj) => obj is Color other && Equals(other);
    public override int GetHashCode() => HashCode.Combine(R, G, B);
}

L'implementazione fornisce tre membri obbligatori: Equals(T?) come confronto principale, override Equals(object?) per le chiamate a livello di oggetto e override GetHashCode() quindi le raccolte basate su hash funzionano correttamente. HashCode.Combine è un helper di libreria che compila un hash dagli stessi valori usati da Equals. L'implementazione IEquatable<T> (overload Equals(T?) ) è facoltativa, ma evita il boxing quando i chiamanti hanno già il tipo concreto.

Quando si definiscono == anche e !=, il linguaggio li richiede come coppia. Gli avvisi CS0660 e CS0661 ricordano di mantenere coerenti tutti e quattro i membri.

Con i tre membri sopra indicati, Equals riflette l'uguaglianza dei valori, ma == verifica comunque l'identità perché non è stato ancora dichiarato alcun == operatore:

var red1 = new Color(255, 0, 0);
var red2 = new Color(255, 0, 0);

Console.WriteLine(red1.Equals(red2)); // => True
Console.WriteLine(red1 == red2);      // => False  (no == overload; identity check)

Un'implementazione corretta deve soddisfare anche il contratto di equivalenza (si supponga xche , ye z siano non Null):

  1. Riflessivo: x.Equals(x) restituisce true.
  2. Simmetrico: x.Equals(y) restituisce lo stesso valore di y.Equals(x).
  3. Transitivo: se x.Equals(y) e y.Equals(z) sono entrambi true, deve x.Equals(z) essere true.
  4. Coerente: chiamate successive per x.Equals(y) restituire lo stesso valore purché nessuno degli oggetti cambi.
  5. Comportamento Null: x.Equals(null) restituisce false; x.Equals(y) non deve generare quando viene chiamato su un oggetto non Null x.

L'uguaglianza dei valori in una gerarchia di classi non bloccate richiede più attenzione rispetto a una classe sealed per soddisfare le regole simmetriche e transitive. Il rischio è che IEquatable<T>.Equals(T? other) dispatch segue il tipo dichiarato (il tipo scritto nella dichiarazione di variabile) della variabile, non il tipo di runtime. Se Shape dichiara un valore diversovirtualEquals(Shape? other) da , una variabile tipizzata come Shape che contiene un oggetto Circle in fase di esecuzione richiama Shape.Equalsin modo Circleinvisibile all'utente i campi specifici. Due Circle oggetti con raggi diversi possono essere confrontati come uguali quando si accede tramite una Shape variabile.

Il modello corretto richiede due requisiti di cooperazione: rendere il metodo virtual tipizzato Equals in modo che ogni classe derivata possa estendere il confronto e aggiungere una GetType() == other.GetType() protezione nell'implementazione della classe base in modo che gli oggetti di tipi di runtime diversi non vengano mai considerati uguali.

Implementazione della classe di base

// Shape is an unsealed base class. Making Equals virtual and guarding with GetType()
// ensures a derived instance is never equal to an instance of a different runtime type.
class Shape : IEquatable<Shape>
{
    public string Color { get; }
    public Shape(string color) => Color = color;

    public override bool Equals(object? obj) => Equals(obj as Shape);

    // virtual so derived classes can override and augment the comparison
    public virtual bool Equals(Shape? other) =>
        other is not null &&
        GetType() == other.GetType() &&   // reject different runtime types
        Color == other.Color;

    // GetType() is included because equality requires matching runtime types
    public override int GetHashCode() => HashCode.Combine(GetType(), Color);

    public static bool operator ==(Shape? l, Shape? r) => l?.Equals(r) ?? r is null;
    public static bool operator !=(Shape? l, Shape? r) => !(l == r);
}

Punti principali:

  • virtual tipizzato Equals: ogni classe derivata esegue l'override di questo metodo per aumentare il confronto con i propri campi.
  • GetType() guard: GetType() == other.GetType() impedisce a un oggetto Circle di eseguire l'uguaglianza di un Shape oggetto con lo stesso colore e impedisce agli oggetti di tipi derivati diversi di essere uguali tra loro.
  • GetHashCode include GetType(): poiché due oggetti sono uguali solo quando i relativi tipi di runtime corrispondono, GetHashCode devono eseguire l'hashing del tipo di runtime e i campi dati. L'omissione GetType() in questo caso causa un comportamento errato in Dictionary<TKey,TValue> e HashSet<T>.
  • == delegati a Equals: mantiene coerente l'uguaglianza tra operatore e metodo.

Implementazione della classe derivata

Una classe derivata che aggiunge campi esegue l'override di , esegue il Equalscast al proprio tipo, chiama base.Equals, quindi confronta i propri campi:

class Circle : Shape
{
    public double Radius { get; }
    public Circle(string color, double radius) : base(color) => Radius = radius;

    public override bool Equals(object? obj) => Equals(obj as Shape);

    // Calls base.Equals to verify Color and runtime type, then adds Radius
    public override bool Equals(Shape? other) =>
        other is Circle c && base.Equals(c) && Radius == c.Radius;

    public override int GetHashCode() => HashCode.Combine(GetType(), Color, Radius);
}

base.Equals(c) applica la GetType() protezione e controlla i campi condivisi. Il cast tramite other is Circle c ha esito negativo rapidamente quando l'argomento è di Shape qualsiasi altro tipo derivato.

Utilizzo tramite una variabile di tipo base

Shape circle1 = new Circle("red", 5.0);
Shape circle2 = new Circle("red", 7.0);
Shape circle3 = new Circle("red", 5.0);
Shape shape1  = new Shape("red");

Console.WriteLine(circle1.Equals(circle2)); // => False  (Radius differs)
Console.WriteLine(circle1.Equals(circle3)); // => True
Console.WriteLine(circle1.Equals(shape1));  // => False  (different runtime types)

Le classi sealed sono più semplici

Non è possibile sottoclassare una sealed classe, quindi i tipi di runtime e in fase di compilazione sono sempre d'accordo. Non hai bisogno della guardia o virtual dell'invioGetType(). Il IEquatable<T> modello illustrato in Implementare l'uguaglianza manualmente quando un tipo non può essere un record è corretto e completo per una classe sealed.

Overload degli operatori

È possibile eseguire l'overload degli == operatori e != in un tipo definito dall'utente. Se si esegue l'overload di uno di questi due operatori, è necessario eseguire anche l'overload dell'altro operatore.

Non è possibile eseguire in modo esplicito l'overload degli == operatori e != in un tipo di record. Per modificare il comportamento degli == operatori e != per il tipo di Trecord , implementare il IEquatable<T>.Equals metodo con la firma seguente:

public virtual bool Equals(T? other);

Specifiche del linguaggio C#

Per altre informazioni, vedere la sezione Operatori relazionali e di test del tipo della specifica del linguaggio C#.

Per altre informazioni sull'uguaglianza dei tipi di record, vedere la sezione Membri di uguaglianza della specifica del linguaggio C#.

Vedi anche