Confronti di uguaglianza in C#

Tip

Questo articolo fa parte della sezione Nozioni fondamentali per gli sviluppatori che già conoscono almeno un linguaggio di programmazione e stanno imparando C#. Se non si ha familiarità con la programmazione, iniziare prima con le esercitazioni introduttive .

Venire da un'altra lingua? In Java, == sugli oggetti e === sugli oggetti in JavaScript verificano l'identità, non il contenuto. Le classi C# funzionano allo stesso modo per impostazione predefinita. In Python, == chiama __eq__ e per impostazione predefinita confronta il contenuto, analogamente a come vengono confrontati i record in C#. Gli struct C# vengono confrontati anche in base al valore quando si chiama Equals.

C# distingue due tipi di uguaglianza. L'uguaglianza dei valori indica che due istanze sono uguali quando i dati corrispondono. L'uguaglianza dei riferimenti indica che due variabili sono uguali solo quando puntano allo stesso oggetto in memoria. Questa condizione è detta anche identity. I tipi valore in genere confrontano i dati e i tipi riferimento confrontano in genere l'identità. Gli autori di tipi possono modificare tali impostazioni predefinite, ma tale modello mentale impedisce bug sottili in cui due oggetti che sembrano identici non sono considerati uguali o in cui una mutazione tramite una variabile cambia automaticamente ciò che un'altra variabile vede.

Tipi di valore, tipi di riferimento e valori predefiniti di uguaglianza

Ogni tipo in C# è un tipo valore o un tipo riferimento. Un tipo di valore contiene i dati direttamente nella variabile. Un tipo riferimento contiene un riferimento a un oggetto . Quando si assegna una variabile di tipo riferimento a un'altra variabile, entrambe le variabili fanno riferimento allo stesso oggetto. Per altre informazioni sui tipi valore e sui tipi di riferimento, vedere Panoramica del sistema dei tipi.

Il comportamento di uguaglianza predefinito segue in genere il tipo di tipo :

  • I tipi numerici predefiniti e le enumerazioni sono tipi valore. Due int variabili sono uguali quando i valori numerici corrispondono.
  • Le struct sono tipi di valore. Un semplice struct usa l'uguaglianza per valore quando chiami Equals.
  • Le tuple sono tipi di valore. Due tuple sono uguali quando tutti i valori degli elementi corrispondono.
  • Le classi sono tipi di riferimento. Una classe normale usa l'uguaglianza dei riferimenti e == quindi Equals verifica se due variabili puntano allo stesso oggetto.

Una classe usa l'uguaglianza dei riferimenti. Due oggetti separati con gli stessi dati non sono uguali, ma due variabili che fanno riferimento allo stesso oggetto sono uguali:

var order1 = new Order(42, "Shoes");
var order2 = new Order(42, "Shoes");

Console.WriteLine(order1 == order2);               // => False
Console.WriteLine(order1.Equals(order2));          // => False
Console.WriteLine(ReferenceEquals(order1, order2)); // => False

Order order3 = order1;
Console.WriteLine(order1 == order3);               // => True

Un struct mostra l'uguaglianza di valore attraverso Equals. Due istanze di struct sono uguali quando i relativi campi corrispondono:

var pt1 = new Point(3, 4);
var pt2 = new Point(3, 4);

Console.WriteLine(pt1.Equals(pt2)); // => True

Gli struct semplici non ottengono un operatore predefinito == . La scrittura p1 == p2 in uno struct normale viene compilata solo se lo struct dichiara il proprio operator ==. Se sono necessari confronti degli operatori per uno struct, definire == e != come coppia e mantenerli coerenti con Equals e GetHashCode.

Anche le tuple sono tipi di valore. Due tuple sono uguali quando il valore di ogni elemento coincide. I nomi degli elementi in una tupla denominata costituiscono una comodità in fase di compilazione e non vengono considerati nel confronto. Solo le posizioni e i valori sono importanti:

var t1 = (Name: "Grace", Role: "Engineer");
var t2 = (Name: "Grace", Role: "Engineer");

Console.WriteLine(t1 == t2); // => True

Per altre informazioni sulla sintassi delle tuple e sulla decostruzione, vedere Tuple e decostruzione.

Usare Object.ReferenceEquals per testare direttamente l'identità

ReferenceEquals verifica sempre l'identità indipendentemente da come un tipo ridefinisce Equals o sovraccarica ==. Usarlo come diagnostica delle identità quando è necessario verificare se due variabili puntano esattamente allo stesso oggetto:

var doc1 = new Document("Report");
var doc2 = new Document("Report");
var doc3 = doc1;

Console.WriteLine(ReferenceEquals(doc1, doc2)); // => False
Console.WriteLine(ReferenceEquals(doc1, doc3)); // => True

Un uso comune consiste nell'usare un Equals override per evitare il confronto completo: quando entrambi gli argomenti fanno riferimento allo stesso oggetto, risultano sempre uguali senza controllare i singoli campi.

Annotazioni

Quando le variabili vengono tipate come interfaccia, == controlla se le variabili di interfaccia fanno riferimento allo stesso oggetto. Una chiamata a Equals esegue ancora l'implementazione dell'oggetto sottostante.

Annotazioni

ReferenceEquals restituisce false sempre quando si confrontano i tipi valore, anche se entrambi gli argomenti contengono gli stessi valori. Questo comportamento si verifica perché ogni argomento di tipo valore viene convertito indipendentemente tramite boxing in un oggetto separato nell'heap quando viene passato a ReferenceEquals.

I tipi possono definire una semantica di uguaglianza diversa

I tipi possono definire la semantica di uguaglianza che differiscono dal comportamento predefinito. Il motivo più comune è implementare l'uguaglianza dei valori. Se si crea un tipo che rappresenta dati, ad esempio un conto bancario, un prodotto presente nell'inventario o un utente in un sistema, considerare uguali le istanze con gli stessi valori. Scegliere i tipi di record per implementare l'uguaglianza dei valori e il compilatore genera automaticamente tutti i membri di uguaglianza necessari.

Annotazioni

Le stringhe sono classi, ma == e Equals confrontano il contenuto della stringa, non l'identità.

  • ==: operatore di uguaglianza. La maggior parte dei tipi usa questo operatore come verifica principale di uguaglianza. Il comportamento dipende dal fatto che il tipo abbia un operatore predefinito o definito dall'utente == .
  • !=: operatore di disuguaglianza. Quando un tipo definisce un operatore definito dall'utente == , deve anche definire !=.
  • Equals: metodo virtuale ereditato da ogni tipo. È possibile eseguirne l'override per modificare la semantica di uguaglianza per un tipo.
  • GetHashCode: metodo virtuale usato dalle raccolte basate su hash. Quando due valori sono uguali, anche i codici hash devono essere uguali.
  • ReferenceEquals: metodo statico che verifica sempre l'identità.

Usare i record per l'uguaglianza dei valori

Usare il record modificatore per assegnare un'uguaglianza del valore del tipo incentrato sui dati quando il tipo può essere un record. Il compilatore genera Equalsmembri , GetHashCodee ==/!= che confrontano ogni valore di proprietà dichiarato.

Un record class è ancora un tipo di riferimento, ma confronta i valori anziché l'identità:

var person1 = new Person("Ada", "Lovelace");
var person2 = new Person("Ada", "Lovelace");

Console.WriteLine(person1 == person2);               // => True
Console.WriteLine(person1.Equals(person2));          // => True
Console.WriteLine(ReferenceEquals(person1, person2)); // => False

ReferenceEquals conferma che person1 e person2 sono oggetti diversi in memoria, mentre == e Equals restituiscono True perché l'uguaglianza generata dal compilatore confronta i valori delle proprietà.

La stessa generazione del compilatore si applica ai record struct tipi:

var dim1 = new Dimension(1920, 1080);
var dim2 = new Dimension(1920, 1080);

Console.WriteLine(dim1 == dim2);      // => True
Console.WriteLine(dim1.Equals(dim2)); // => True

I tipi di record generano l'intero set di uguaglianza per il proprio tipo. Sia il tipo record class che il tipo record struct sovrascrivono Equals e GetHashCode. Generano anche gli operatori == e !=, oltre a un metodo Equals tipizzato per il tipo record. A differenza di un semplice struct, un record struct supporta pertanto automaticamente == e !=. Per altre informazioni sui tipi di record e sulla relativa semantica di uguaglianza, vedere Record.

Record con membri di tipo riferimento

L'uguaglianza dei record usa la semantica di uguaglianza propria dei membri. Ogni proprietà o campo viene confrontato usando il proprio Equals metodo. Per la maggior parte dei valori scalari, ad esempio int, stringo DateTime, questo approccio confronta i valori dei membri del record. La sottigliezza si verifica con raccolte modificabili comuni, List<T> ad esempio o T[]: questi tipi vengono confrontati per riferimento, quindi due istanze di record che contengono oggetti elenco diversi con lo stesso contenutonon vengono considerate uguali dall'uguaglianza dei record sintetizzati.

var playlist1 = new Playlist("Chill", new List<string> { "Song A", "Song B" });
var playlist2 = new Playlist("Chill", new List<string> { "Song A", "Song B" });

Console.WriteLine(playlist1.Equals(playlist2));                        // => False (different List instances)
Console.WriteLine(playlist1.Tracks.SequenceEqual(playlist2.Tracks));   // => True

playlist1 e playlist2 sono istanze List<string> separate. Anche se il contenuto corrisponde, Equals restituisce false.

Quando è necessaria l'uguaglianza dei record per riflettere il contenuto della raccolta, sono disponibili alcune opzioni:

  • Implementate IEquatable<T> sul record e ridefinite Equals per utilizzare Enumerable.SequenceEqual per i membri della raccolta.
  • Usa un tipo di raccolta con uguaglianza per valore — ad esempio, un IEqualityComparer<T> personalizzato o un tipo il cui Equals confronta gli elementi.
  • Progettazione intorno all'identità: se il record rappresenta un'entità anziché un valore puro, l'uguaglianza dei riferimenti per i membri della raccolta potrebbe essere intenzionale.

Importante

L'implementazione manuale dell'uguaglianza è rara oggi in C#. I record gestiscono automaticamente lo scenario comune di uguaglianza dei valori. Se è necessario implementare manualmente l'uguaglianza, ad esempio perché il tipo deve derivare da una classe di base non record, vedere Implementare l'uguaglianza manualmente quando un tipo non può essere un record nel riferimento al linguaggio.

Vedere anche