Tracce
/
C#
C#
/
Programma
/
Uguaglianza
Ug

Uguaglianza in C#

5 esercizi

Informazioni su Uguaglianza

L'esercizio di programmazione illustra diverse proprietà dell'uguaglianza in C#:

Object.Equals()

Argomenti trattati dall'esercizio

  • I tipi semplici (le stringhe e i tipi primitivi) di solito si confrontano per uguaglianza con == e !=. Questo è considerato più idiomatico rispetto all'uso del metodo Equals(), disponibile anch'esso per questi tipi. Chi programma in Java, quando torna al proprio linguaggio di origine, deve fare attenzione al fatto che, con le stringhe, == confronta per valore in C# ma per riferimento in Java.
  • I tipi di riferimento (le istanze delle classi) si confrontano usando il metodo Equals() ereditato da object. Se con il test di uguaglianza vuoi assicurarti che due oggetti siano esattamente la stessa istanza, allora ti basterà affidarti all'implementazione di object. In caso contrario, devi eseguire l'override di object.Equals().
  • Se sai che tutte le istanze della classe vengono create in un unico punto, per esempio i personaggi di un gioco o di una simulazione, l'uguaglianza per riferimento è sufficiente. Tuttavia è probabile che vengano create più istanze della stessa entità del mondo reale (per esempio da un database, dall'input dell'utente, tramite una richiesta web). In questo caso è necessario confrontare per uguaglianza i valori che identificano l'entità in modo univoco. Perciò è necessario eseguire l'override di Equals().
  • Un Equals() di cui è stato eseguito l'override conterrà test di uguaglianza sui membri di tipi semplici, usando ==, e sui tipi di riferimento, con chiamate ricorsive a Equals().
class StatusBar
{
    private readonly int width = 200, height = 20;

    public override bool Equals(object other)
    {
        // ... null and type checks and performance optimisations
        return width == (other as StatusBar).width && height == (other as StatusBar).height;
    }
}
class Window
{
    private readonly string title = "Main";
    private readonly StatusBar statusBar = new StatusBar();

    public override bool Equals(object other)
    {
        // ... null and type checks and performance optimisations
        return title == (other as Window).title && statusBar.Equals((other as Window).statusBar);
    }
}

  • Il metodo statico object.ReferenceEquals() si usa per confrontare due oggetti e stabilire se sono una sola e medesima istanza. Questo rende più chiaro il codice ed è indispensabile quando Equals() e == sono stati sottoposti a override o sovraccaricati.
var winA = new Window(); // above code shows that all windows are equal
var winB = new Window();
ReferenceEquals(winA, winB);
// => false
var winC = winA;
ReferenceEquals(winA, winC);
// => true

Argomenti accessori

  • Oltre a public override bool Equals(object obj), gli IDE in genere generano l'overload protected bool Equals(FacialFeatures other) da usare quando è coinvolta l'ereditarietà. Una classe derivata può chiamare Equals() della classe base e poi aggiungere il proprio test.
  • Non usare == a meno che tu non abbia sovraccaricato l'operatore ==, oltre che il metodo Equals() nella classe (vedi l'esercizio operator-overloading), o a meno che non ti interessi soltanto che i riferimenti siano uguali.
  • I test di uguaglianza per structs sono trattati nell'esercizio structs.
  • L'uguaglianza delle tuples è trattata nell'esercizio tuples.
  • Molti sviluppatori si affidano al proprio IDE per l'implementazione dei metodi di uguaglianza, dato che questo si occupa di tutti i dettagli più minuti. Per esempio, RIDER di JetBrains (v 2020.1) genera i seguenti metodi di uguaglianza per una classe:
protected bool Equals(T other)
{
    return field1 == other.field1 && field2.Equals(other.field2);
}

public override bool Equals(object obj)
{
    if (ReferenceEquals(null, obj)) return false;
    if (ReferenceEquals(this, obj)) return true;
    if (obj.GetType() != this.GetType()) return false;
    return Equals((T) obj);
}
  • Fai attenzione se decidi di migliorare il codice generato dal tuo IDE. In genere quel codice è a prova di null e di oggetti dal tipo errato, ed è anche piuttosto ben ottimizzato.
  • I test per l'uguaglianza dei delegati non sono trattati specificamente nell'esercizio.
  • Non esistono test di uguaglianza integrati per gli array né per la maggior parte delle collezioni. LINQ (di cui parleremo in esercizi successivi) fornisce SequenceEquals(), ma in assenza di LINQ occorre iterare su entrambe le collezioni e confrontare gli elementi uno per uno.
  • Per una trattazione di come usare == e != con le proprie classi, vedi l'esercizio operator-overloading.

Object.GetHashCode()

  • object.GetHashCode() restituisce un codice hash sotto forma di numero intero a 32 bit. Il codice hash è usato dalle classi dizionario e set, come Dictionary<T> e HashSet<T>, per archiviare e recuperare oggetti in modo efficiente. Nel caso dei dizionari, l'hashing riguarda le chiavi.
  • Tra chi sviluppa in C# c'è l'aspettativa che, se si esegue l'override di Equals(), si esegua l'override anche di GetHashCode(). Esiste una relazione tra Equals() e GetHashCode() che deve valere perché le classi dizionario e set di hash, e ogni altra che usi un codice hash, si comportino correttamente. Ci si aspetta che il metodo venga implementato in modo da non tendere trappole ai manutentori che in futuro potrebbero aggiungere una collezione basata su codice hash.
  • La relazione tra codice hash e uguaglianza è questa: se due oggetti sono uguali (Equal() restituisce true), allora GetHashCode() dei due oggetti deve restituire lo stesso valore. Questo non vale nel verso opposto. Non è una relazione simmetrica. Immagina una funzione di ricerca che prima va in un «bucket» in base al codice hash e poi seleziona l'elemento specifico usando il test di uguaglianza.
  • Il modo più semplice per creare un codice hash è chiamare HashCode.Combine() passando i valori usati nel test di uguaglianza (o un sottoinsieme). Tieni presente che più informazioni fornisci a Combine(), più è probabile che l'implementazione dell'hash sia efficiente.
public class Assessment
{
    private int rating;
    private Person boss;

    public override int GetHashCode()
    {
        return HashCode.Combine(rating, boss);
    }
}
  • I valori usati nel test di uguaglianza devono rimanere stabili finché la collezione con hash è in uso. Se aggiungi un oggetto alla collezione con un insieme di valori e poi cambi quei valori, il codice hash non punterà più al «bucket» corretto. In pratica questo significa che l'oggetto dovrebbe essere immutabile. Altri approcci rischiano di creare trabocchetti per i manutentori. L'immutabilità è trattata in altri esercizi.
  • È possibile progettare un codice hash migliore di quello prodotto dalle routine della libreria, ma o lo si fa perché si conoscono in dettaglio le caratteristiche dei dati, o perché la collezione è molto semplice e i valori si possono usare direttamente senza hashing. Potrebbe non valere la pena dello sforzo aggiuntivo.

Miglioramenti delle prestazioni

Per migliorare leggermente le prestazioni, soprattutto quando gli oggetti appartengono a collezioni, puoi aggiungere un membro di overload, public bool Equals(T other).

Questo farà risparmiare una certa quantità di controlli di tipo per i tipi di riferimento e un passaggio di boxing per i tipi valore, perché non dovranno essere convertiti in un oggetto (boxed) come argomento di public override bool Equals(object other).

Se aggiungi l'interfaccia IEquatable<T> a una classe, questa richiederà che l'overload venga implementato. A meno che il codice non contenga routine che accettano oggetti di tipo IEquatable<T> (e che presumibilmente fanno qualcosa di interessante legato all'uguaglianza, indipendentemente dalla classe che la implementa), non c'è davvero nessun altro motivo valido per includere l'interfaccia.

IEqualityComparer<T>

Se hai una classe che può essere identificata in modo univoco in due modi diversi, per esempio una classe Person con un SSID e un indirizzo email univoco, .NET offre un modo per far sì che due collezioni diverse usino test di uguaglianza e codici hash diversi. Ciascuna può avere un'implementazione diversa di IEqualityComparer<T>, con i propri metodi Equals() e GetHashCode(). Puoi avere un dizionario con chiave il SSID e un altro con chiave l'indirizzo email.

Quando si usa IEqualityComparer<T>, in genere si esegue comunque l'override di Equals() e GetHashCode() nella classe dell'elemento, per evitare problemi al di fuori delle classi di collezione.

Una considerazione da fare quando si usa IEqualityComparer<T> è che i metodi privati e simili non saranno disponibili per il test di uguaglianza.

Se è in gioco una sola collezione con hash, forse è meglio evitare IEqualityComparer<T> e incapsulare uguaglianza e codice hash nell'oggetto stesso. Non è l'ideale, tanto per cominciare, che una dipendenza fondamentale come quella tra oggetto e collezione con hash non possa essere imposta dal compilatore; ma con la logica di hashing e confronto altrove, sarebbe ancora più facile per un manutentore modificare i membri di una classe senza considerare le conseguenze sulla collezione.

Nota sull'uguaglianza in virgola mobile

Una primitiva che può mettere in difficoltà chi programma senza fare attenzione è il test di uguaglianza dei valori in virgola mobile. Se ne parla nel documento about.md del concetto floating-point-numbers.

Uguaglianza ed ereditarietà

Questo articolo mostra alcune delle decisioni da prendere in merito a uguaglianza ed ereditarietà. Ti riguarderanno solo se stai manipolando istanze di una classe e di una classe derivata e devi testare l'uguaglianza tra le due.

Modifica tramite GitHub Il collegamento si apre in una nuova finestra o scheda

Impara Uguaglianza

La pratica è bloccata

Sblocca 4 altri esercizi per esercitarti su Uguaglianza