Domanda

Di recente mi sono imbattuto in questo, fino ad ora ho felicemente ignorato l'operatore di uguaglianza ( == ) e / o Uguali per vedere se due riferimenti i tipi in realtà contenevano gli stessi dati (ovvero due istanze diverse che sembrano uguali).

Lo sto usando ancora di più da quando ho approfondito i test automatizzati (confrontando i dati di riferimento / previsti con quelli restituiti).

Osservando alcune delle linee guida sugli standard di codifica in MSDN Mi sono imbattuto in un articolo che sconsiglia. Ora capisco perché l'articolo dice questo (perché non sono la stessa istanza ) ma non risponde alla domanda:

  1. Qual è il modo migliore per confrontare due tipi di riferimento?
  2. Dovremmo implementare IComparable ? (Ho anche visto menzionare che questo dovrebbe essere riservato solo ai tipi di valore).
  3. C'è qualche interfaccia che non conosco?
  4. Dovremmo semplicemente lanciare il nostro ?!

Molte grazie ^ _ ^

Aggiornamento

Sembra che avessi letto male una parte della documentazione (è stata una lunga giornata) e ignorato Uguali potrebbe essere la strada da percorrere ..

  

Se si sta implementando il riferimento   tipi, dovresti considerare l'override   il metodo Equals su un tipo di riferimento   se il tuo tipo sembra un tipo di base   come Point, String, BigNumber,   e così via. La maggior parte dei tipi di riferimento dovrebbe   non sovraccaricare l'operatore uguaglianza ,   anche se hanno la precedenza su Uguali . Però,   se stai implementando un riferimento   tipo che deve avere valore   semantica, come un numero complesso   tipo, dovresti sovrascrivere l'uguaglianza   operatore.

È stato utile?

Soluzione

Sembra che tu stia codificando in C #, che ha un metodo chiamato Equals che la tua classe dovrebbe implementare, se vuoi confrontare due oggetti usando qualche altra metrica di " sono questi due puntatori (perché gli handle degli oggetti sono proprio questo , puntatori) allo stesso indirizzo di memoria? " ;.

Ho preso un po 'di codice di esempio da qui :

class TwoDPoint : System.Object
{
    public readonly int x, y;

    public TwoDPoint(int x, int y)  //constructor
    {
        this.x = x;
        this.y = y;
    }

    public override bool Equals(System.Object obj)
    {
        // If parameter is null return false.
        if (obj == null)
        {
            return false;
        }

        // If parameter cannot be cast to Point return false.
        TwoDPoint p = obj as TwoDPoint;
        if ((System.Object)p == null)
        {
            return false;
        }

        // Return true if the fields match:
        return (x == p.x) && (y == p.y);
    }

    public bool Equals(TwoDPoint p)
    {
        // If parameter is null return false:
        if ((object)p == null)
        {
            return false;
        }

        // Return true if the fields match:
        return (x == p.x) && (y == p.y);
    }

    public override int GetHashCode()
    {
        return x ^ y;
    }
}

Java ha meccanismi molto simili. Il metodo equals () fa parte della classe Oggetto e la tua classe lo sovraccarica se desideri questo tipo di funzionalità.

Il motivo del sovraccarico '==' può essere una cattiva idea per gli oggetti è che, di solito, vuoi comunque essere in grado di fare il "sono questi stessi puntatori" confronti. Questi sono generalmente fatti affidamento, ad esempio, sull'inserimento di un elemento in un elenco in cui non sono consentiti duplicati e alcuni elementi del framework potrebbero non funzionare se questo operatore è sovraccarico in modo non standard.

Altri suggerimenti

L'implementazione dell'uguaglianza in .NET in modo corretto, efficiente e senza duplicazione del codice è difficile. In particolare, per i tipi di riferimento con semantica di valore (ovvero tipi immutabili che trattano l'equvialenza come uguaglianza ), dovresti implementare l'interfaccia System.IEquatable < T > e dovresti implementare tutte le diverse operazioni ( Equals , GetHashCode e == , ! = ).

Ad esempio, ecco una classe che implementa l'uguaglianza di valore:

class Point : IEquatable<Point> {
    public int X { get; }
    public int Y { get; }

    public Point(int x = 0, int y = 0) { X = x; Y = y; }

    public bool Equals(Point other) {
        if (other is null) return false;
        return X.Equals(other.X) && Y.Equals(other.Y);
    }

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

    public static bool operator ==(Point lhs, Point rhs) => object.Equals(lhs, rhs);

    public static bool operator !=(Point lhs, Point rhs) => ! (lhs == rhs);

    public override int GetHashCode() => X.GetHashCode() ^ Y.GetHashCode();
}

Le uniche parti mobili nel codice sopra sono le parti in grassetto: la seconda riga in Equals (Point other) e il metodo GetHashCode () . L'altro codice dovrebbe rimanere invariato.

Per le classi di riferimento che non rappresentano valori immutabili, non implementare gli operatori == e ! = . Invece, usa il loro significato predefinito, che è quello di confrontare l'identità dell'oggetto.

Il codice intenzionalmente identifica anche gli oggetti di un tipo di classe derivata. Spesso, ciò potrebbe non essere desiderabile perché l'uguaglianza tra la classe base e le classi derivate non è ben definita. Sfortunatamente, .NET e le linee guida per la codifica non sono molto chiari qui. Il codice creato da Resharper, pubblicato in un'altra risposta , è suscettibile a comportamenti indesiderati in questi casi perché è uguale a (oggetto x) e Equals (SecurableResourcePermission x) tratteranno questo caso in modo diverso.

Per modificare questo comportamento, è necessario inserire un ulteriore controllo del tipo nel metodo Equals fortemente tipizzato sopra:

public bool Equals(Point other) {
    if (other is null) return false;
    if (other.GetType() != GetType()) return false;
    return X.Equals(other.X) && Y.Equals(other.Y);
}

Di seguito ho riassunto ciò che è necessario fare durante l'implementazione di IEquatable e fornito la giustificazione dalle varie pagine della documentazione MSDN.


Sommario

  • Quando si desidera verificare l'uguaglianza dei valori (ad esempio quando si utilizzano oggetti nelle raccolte) è necessario implementare l'interfaccia IEquatable, sovrascrivere Object.Equals e GetHashCode per la propria classe.
  • Quando si desidera verificare l'uguaglianza di riferimento, è necessario utilizzare operator ==, operator! = e Object.ReferenceEquals .
  • Dovresti sovrascrivere solo l'operatore == e l'operatore! = per ValueTypes e tipi di riferimento immutabili.

Motivazione

IEquatable

  

L'interfaccia System.IEquatable viene utilizzata per confrontare due istanze di un oggetto per l'uguaglianza. Gli oggetti vengono confrontati in base alla logica implementata nella classe. Il confronto si traduce in un valore booleano che indica se gli oggetti sono diversi. Ciò è in contrasto con l'interfaccia System.IComparable, che restituisce un numero intero che indica come i valori degli oggetti sono diversi.

     

L'interfaccia IEquatable dichiara due metodi che devono essere sovrascritti. Il metodo Equals contiene l'implementazione per eseguire il confronto effettivo e restituire true se i valori dell'oggetto sono uguali o false se non lo sono. Il metodo GetHashCode dovrebbe restituire un valore hash univoco che può essere utilizzato per identificare in modo univoco oggetti identici che contengono valori diversi. Il tipo di algoritmo di hashing utilizzato è specifico dell'implementazione.

Metodo IEquatable.Equals

  
      
  • Dovresti implementare IEquatable per i tuoi oggetti per gestire la possibilità che vengano archiviati in un array o in una raccolta generica.
  •   
  • Se si implementa IEquatable, è necessario sostituire anche le implementazioni della classe base di Object.Equals (Object) e GetHashCode in modo che il loro comportamento sia coerente con quello del metodo IEquatable.Equals
  •   

Linee guida per la sostituzione di uguali () e Operatore == (C # Guida alla programmazione)

  
      
  • x.Equals (x) restituisce true.
  •   
  • x.Equals (y) restituisce lo stesso valore di y.Equals (x)
  •   
  • if (x.Equals (y) & amp; & amp; y.Equals (z)) restituisce true, quindi x.Equals (z) restituisce true.
  •   
  • Successive invocazioni di x. Equals (y) restituisce lo stesso valore purché gli oggetti a cui fanno riferimento xey non vengano modificati.
  •   
  • x. Equals (null) restituisce false (solo per tipi di valore non annullabili. Per ulteriori informazioni, vedere Tipi nullabili (Guida per programmatori C #) .)
  •   
  • La nuova implementazione di Equals non dovrebbe generare eccezioni.
  •   
  • Si consiglia che qualsiasi classe che sovrascrive Equals sovrascriva anche Object.GetHashCode.
  •   
  • Si raccomanda che oltre all'implementazione di Equals (oggetto), qualsiasi classe implementi anche Equals (tipo) per il proprio tipo, per migliorare le prestazioni.
  •   
     

Per impostazione predefinita, l'operatore == verifica l'uguaglianza di riferimento determinando se due riferimenti indicano lo stesso oggetto. Pertanto, i tipi di riferimento non devono implementare l'operatore == per ottenere questa funzionalità. Quando un tipo è immutabile, cioè i dati contenuti nell'istanza non possono essere modificati, sovraccaricare l'operatore == per confrontare l'uguaglianza di valore anziché l'uguaglianza di riferimento può essere utile perché, come oggetti immutabili, possono essere considerati uguali a lungo in quanto hanno lo stesso valore. Non è una buona idea sostituire l'operatore == in tipi non immutabili.

     
      
  • Le implementazioni dell'operatore sovraccaricato == non devono generare eccezioni.
  •   
  • Qualsiasi tipo che sovraccarica l'operatore == dovrebbe anche sovraccaricare l'operatore! =.
  •   

== Operatore (riferimento C #)

  
      
  • Per tipi di valore predefiniti, l'operatore di uguaglianza (==) restituisce vero se i valori dei suoi operandi sono uguali, falso altrimenti.
  •   
  • Per tipi di riferimento diversi da string, == restituisce true se i suoi due operandi si riferiscono allo stesso oggetto.
  •   
  • Per il tipo di stringa, == confronta i valori delle stringhe.
  •   
  • Quando si esegue il test di null utilizzando == confronti all'interno del proprio operatore == sostituzioni, assicurarsi di utilizzare l'operatore della classe di oggetti di base. In caso contrario, si verificherà una ricorsione infinita con il risultato di uno stackoverflow.
  •   

Metodo Object.Equals (Object)

  

Se il linguaggio di programmazione supporta il sovraccarico dell'operatore e se si sceglie di sovraccaricare l'operatore di uguaglianza per un determinato tipo, quel tipo deve sovrascrivere il metodo Equals. Tali implementazioni del metodo Equals devono restituire gli stessi risultati dell'operatore di uguaglianza

     

Le seguenti linee guida sono per l'implementazione di un tipo di valore :

     
      
  • Prendi in considerazione la possibilità di sostituire Equals per ottenere prestazioni migliori rispetto a quelle fornite dall'implementazione predefinita di Equals su ValueType.
  •   
  • Se si sostituisce Equals e la lingua supporta il sovraccarico dell'operatore, è necessario sovraccaricare l'operatore di uguaglianza per il proprio tipo di valore.
  •   
     

Le seguenti linee guida sono per l'implementazione di un tipo di riferimento :

     
      
  • Prendi in considerazione la possibilità di sostituire Equals su un tipo di riferimento se la semantica del tipo si basa sul fatto che il tipo rappresenta alcuni valori.
  •   
  • La maggior parte dei tipi di riferimento non deve sovraccaricare l'operatore di uguaglianza, anche se hanno la precedenza su Uguali. Tuttavia, se si sta implementando un tipo di riferimento che intende avere una semantica di valore, come un tipo di numero complesso, è necessario ignorare l'operatore di uguaglianza.
  •   

Gotcha aggiuntivi

Quell'articolo raccomanda solo di non eseguire l'override dell'operatore di uguaglianza (per i tipi di riferimento), non di ignorare Equals. Dovresti sovrascrivere Equals nel tuo oggetto (riferimento o valore) se i controlli di uguaglianza significheranno qualcosa di più dei controlli di riferimento. Se desideri un'interfaccia, puoi anche implementare IEquatable (utilizzato da collezioni generiche). Se si implementa IEquatable, tuttavia, è necessario sovrascrivere uguali, come indicato nella sezione Osservazioni di IEquatable:

  

Se implementi IEquatable < T > ;, dovresti anche sovrascrivere le implementazioni della classe base di Object.Equals (Object) e GetHashCode in modo che il loro comportamento sia coerente con quello del metodo IEquatable < T > .Equals. Se si esegue l'override di Object.Equals (Object), l'implementazione sostituita viene anche chiamata nelle chiamate al metodo statico Equals (System.Object, System.Object) sulla classe. Ciò garantisce che tutte le invocazioni del metodo Equals restituiscano risultati coerenti.

Per sapere se è necessario implementare Equals e / o l'operatore di uguaglianza:

Da Implementazione del metodo Equals

  

La maggior parte dei tipi di riferimento non deve sovraccaricare l'operatore di uguaglianza, anche se ha la precedenza su Uguali.

Da Linee guida per l'implementazione di Equals and the Equality Operator ( ==)

  

Sovrascrivi il metodo Equals ogni volta che implementi l'operatore di uguaglianza (==) e fai in modo che facciano la stessa cosa.

Questo dice solo che è necessario sovrascrivere Equals ogni volta che si implementa l'operatore di uguaglianza. non dice che devi sovrascrivere l'operatore di uguaglianza quando esegui l'override di Uguali.

Per oggetti complessi che produrranno confronti specifici, l'implementazione di IComparable e la definizione del confronto nei metodi di confronto sono una buona implementazione.

Ad esempio abbiamo " Veicolo " oggetti in cui l'unica differenza potrebbe essere il numero di registrazione e lo utilizziamo per confrontare per garantire che il valore atteso restituito nei test sia quello che vogliamo.

Tendo a usare ciò che Resharper fa automaticamente. per esempio, lo ha creato automaticamente per uno dei miei tipi di riferimento:

public override bool Equals(object obj)
{
    if (ReferenceEquals(null, obj)) return false;
    if (ReferenceEquals(this, obj)) return true;
    return obj.GetType() == typeof(SecurableResourcePermission) && Equals((SecurableResourcePermission)obj);
}

public bool Equals(SecurableResourcePermission obj)
{
    if (ReferenceEquals(null, obj)) return false;
    if (ReferenceEquals(this, obj)) return true;
    return obj.ResourceUid == ResourceUid && Equals(obj.ActionCode, ActionCode) && Equals(obj.AllowDeny, AllowDeny);
}

public override int GetHashCode()
{
    unchecked
    {
        int result = (int)ResourceUid;
        result = (result * 397) ^ (ActionCode != null ? ActionCode.GetHashCode() : 0);
        result = (result * 397) ^ AllowDeny.GetHashCode();
        return result;
    }
}

Se vuoi sovrascrivere == e continuare a fare i controlli di ref, puoi comunque usare Object.ReferenceEquals .

Sembra che Microsoft abbia cambiato la propria melodia, o almeno ci sono informazioni contrastanti sul non sovraccaricare l'operatore di uguaglianza. Secondo questo articolo Microsoft intitolato Procedura: definire l'uguaglianza dei valori per un tipo:

" Gli operatori == e! = possono essere usati con le classi anche se la classe non le sovraccarica. Tuttavia, il comportamento predefinito consiste nell'eseguire un controllo di uguaglianza di riferimento. In una classe, se sovraccarichi il metodo Equals, dovresti sovraccaricare gli operatori == e! =, Ma non è necessario. & Quot;

Secondo Eric Lippert nella sua risposta a una domanda che ho posto su Codice minimo per l'uguaglianza in C # - dice:

" Il pericolo in cui ti imbatti qui è che ottieni un == operatore definito per te che fa riferimento alla parità per impostazione predefinita. Potresti facilmente finire in una situazione in cui un metodo Equals sovraccaricato valuta l'uguaglianza e == fa riferimento all'uguaglianza, e quindi usi accidentalmente l'uguaglianza di riferimento su cose non uguali al riferimento che sono uguali al valore. Questa è una pratica soggetta a errori che è difficile individuare dalla revisione del codice umano.

Un paio di anni fa ho lavorato su un algoritmo di analisi statica per rilevare statisticamente questa situazione e abbiamo riscontrato un tasso di difetto di circa due istanze per milione di righe di codice in tutte le basi di codice che abbiamo studiato. Quando si considerano solo basi di codice che avevano da qualche parte superato Equals, il tasso di difetto era ovviamente considerevolmente più alto!

Inoltre, considera i costi rispetto ai rischi. Se hai già implementazioni di IComparable, scrivere tutti gli operatori è banale con una riga che non avrà bug e non verrà mai modificato. È il codice più economico che tu abbia mai scritto. Se viene data la scelta tra il costo fisso di scrittura e test di una dozzina di metodi minuscoli rispetto al costo illimitato di trovare e correggere un bug difficile da vedere in cui viene utilizzata l'uguaglianza di riferimento anziché l'uguaglianza di valore, so quale sceglierei. & Quot ;

.NET Framework non utilizzerà mai == o! = con nessun tipo che scrivi. Ma il pericolo è cosa succederebbe se lo facesse qualcun altro. Quindi, se la classe è per una terza parte, fornirei sempre gli operatori == e! =. Se la classe dovesse essere utilizzata solo internamente dal gruppo, probabilmente implementerei ancora gli operatori == e! =.

Implementerei gli operatori < ;, < =, > ;, e > = solo se fosse implementato IComparable. IComparable dovrebbe essere implementato solo se il tipo deve supportare l'ordinamento, ad esempio quando si ordina o si utilizza in un contenitore generico ordinato come SortedSet.

Se il gruppo o la società avesse messo in atto una politica per non implementare mai gli operatori == e! = - ovviamente seguirei quella politica. Se tale politica fosse in atto, sarebbe saggio applicarla con uno strumento di analisi del codice Q / A che contrassegna qualsiasi occorrenza degli operatori == e! = Quando utilizzato con un tipo di riferimento.

Credo che ottenere qualcosa di semplice come controllare la correttezza degli oggetti sia un po 'complicato con il design di .NET.

For Struct

1) Implementa IEquatable < T > . Migliora notevolmente le prestazioni.

2) Dato che ora hai il tuo Equals , sovrascrivi GetHashCode e per essere coerente con i vari controlli di uguaglianza sovrascrivi object.Equals pure.

3) Non è necessario eseguire religiosamente il sovraccarico degli operatori == e ! = poiché il compilatore avviserà se si identifica involontariamente una struttura con un'altra con un = = o ! = , ma è bene farlo per essere coerenti con i metodi Equals .

public struct Entity : IEquatable<Entity>
{
    public bool Equals(Entity other)
    {
        throw new NotImplementedException("Your equality check here...");
    }

    public override bool Equals(object obj)
    {
        if (obj == null || !(obj is Entity))
            return false;

        return Equals((Entity)obj);
    }

    public static bool operator ==(Entity e1, Entity e2)
    {
        return e1.Equals(e2);
    }

    public static bool operator !=(Entity e1, Entity e2)
    {
        return !(e1 == e2);
    }

    public override int GetHashCode()
    {
        throw new NotImplementedException("Your lightweight hashing algorithm, consistent with Equals method, here...");
    }
}

Per classe

Dagli Stati membri:

  

La maggior parte dei tipi di riferimento non deve sovraccaricare l'operatore di uguaglianza, anche se ha la precedenza su Uguali.

Per me == sembra l'uguaglianza di valore, più come uno zucchero sintattico per il metodo Equals . Scrivere a == b è molto più intuitivo che scrivere a.Equals (b) . Raramente dovremo verificare l'uguaglianza di riferimento. Nei livelli astratti che si occupano di rappresentazioni logiche di oggetti fisici questo non è qualcosa che dovremmo controllare. Penso che avere semantiche diverse per == e Equals possa effettivamente confondere. Credo che avrebbe dovuto essere == per l'uguaglianza di valore e Equals come riferimento (o un nome migliore come IsSameAs ) in primo luogo. Mi piacerebbe non prendere sul serio le linee guida sulla SM qui, non solo perché non è naturale per me, ma anche perché sovraccaricare == non fa alcun danno grave. È diverso dal non sovrascrivere Equals o GetHashCode non generici che possono mordere, perché framework non usa == ovunque ma solo se noi stessi usalo. L'unico vero vantaggio che ottengo dal non sovraccarico di == e != sarà la coerenza con il design dell'intero framework su cui non ho controllo di. E questa è davvero una grande cosa, così tristemente che mi atterrò .

Con semantica di riferimento (oggetti mutabili)

1) Sostituisci Equals e GetHashCode .

2) L'implementazione di IEquatable < T > non è un must, ma sarà piacevole se ne hai uno.

public class Entity : IEquatable<Entity>
{
    public bool Equals(Entity other)
    {
        if (ReferenceEquals(this, other))
            return true;

        if (ReferenceEquals(null, other))
            return false;

        //if your below implementation will involve objects of derived classes, then do a 
        //GetType == other.GetType comparison
        throw new NotImplementedException("Your equality check here...");
    }

    public override bool Equals(object obj)
    {
        return Equals(obj as Entity);
    }

    public override int GetHashCode()
    {
        throw new NotImplementedException("Your lightweight hashing algorithm, consistent with Equals method, here...");
    }
}

Con semantica di valore (oggetti immutabili)

Questa è la parte difficile. Può essere facilmente incasinato se non curato ..

1) Sostituisci Equals e GetHashCode .

2) Sovraccarico == e ! = per abbinare Equals . Assicurati che funzioni per null .

2) L'implementazione di IEquatable < T > non è un must, ma sarà piacevole se ne hai uno.

public class Entity : IEquatable<Entity>
{
    public bool Equals(Entity other)
    {
        if (ReferenceEquals(this, other))
            return true;

        if (ReferenceEquals(null, other))
            return false;

        //if your below implementation will involve objects of derived classes, then do a 
        //GetType == other.GetType comparison
        throw new NotImplementedException("Your equality check here...");
    }

    public override bool Equals(object obj)
    {
        return Equals(obj as Entity);
    }

    public static bool operator ==(Entity e1, Entity e2)
    {
        if (ReferenceEquals(e1, null))
            return ReferenceEquals(e2, null);

        return e1.Equals(e2);
    }

    public static bool operator !=(Entity e1, Entity e2)
    {
        return !(e1 == e2);
    }

    public override int GetHashCode()
    {
        throw new NotImplementedException("Your lightweight hashing algorithm, consistent with Equals method, here...");
    }
}

Prestare particolare attenzione a vedere come dovrebbe essere se la tua classe può essere ereditata, in questi casi dovrai determinare se un oggetto di classe base può essere uguale a un oggetto di classe derivato. Idealmente, se nessun oggetto di classe derivata viene utilizzato per il controllo dell'uguaglianza, un'istanza di classe base può essere uguale a un'istanza di classe derivata e in tali casi non è necessario controllare l'uguaglianza Type in generico < codice> Uguale a della classe base.

In generale, fare attenzione a non duplicare il codice. Avrei potuto creare una classe base astratta generica ( IEqualizable < T > o giù di lì) come modello per consentire un riutilizzo più semplice, ma purtroppo in C # che mi impedisce di derivare da classi aggiuntive.

Tutte le risposte di cui sopra non considerano il polimorfismo, spesso si desidera che i riferimenti derivati ??utilizzino gli uguali derivati ??anche se confrontati tramite un riferimento di base. Si prega di consultare la domanda / discussione / risposte qui - Uguaglianza e polimorfismo

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top