2012-03-14 31 views
12

La documentazione VS2005 Guidelines for Overloading Equals() and Operator == (C# Programming Guide) stati in parteQuando dovrebbe essere una classe .NET Override()? Quando non dovrebbe?

Override == operatore nei tipi non immutabili non è raccomandato.

La più recente .NET Framework 4 documentazione Guidelines for Implementing Equals and the Equality Operator (==) omette questa affermazione, anche se un posto in Content Community ripete l'affermazione e riferimenti alla documentazione più vecchio.

Sembra che è ragionevole ignorare Equals() almeno per alcune classi mutabili banali, come

public class ImaginaryNumber 
{ 
    public double RealPart { get; set; } 
    public double ImaginaryPart { get; set; } 
} 

In matematica, due numeri immaginari che hanno la stessa parte reale e la stessa parte immaginaria sono di fatto uguale nel momento in cui viene testata l'uguaglianza. Non è corretto affermare che si tratta di non uguale a, il che accadrebbe se oggetti separati con lo stesso RealPart e ImaginaryPart fossero Equals() non sovrascritti.

D'altra parte, se si esegue l'override di Equals(), è necessario anche eseguire l'override di GetHashCode(). Se un ImaginaryNumber che sostituisce Equals() e GetHashCode() viene inserito in un HashSet e un'istanza mutabile modifica il suo valore, quell'oggetto non sarà più trovato nell'HashSet.

MSDN non corretto per rimuovere le linee guida per non sovrascrivere Equals() e operator== per tipi non immutabili?

È ragionevole sovrascrivere Equals() per i tipi mutabili in cui "nel mondo reale" l'equivalenza di tutte le proprietà significa che gli oggetti stessi sono uguali (come con ImaginaryNumber)?

Se è ragionevole, qual è il modo migliore per gestire la potenziale mutabilità mentre un'istanza di un oggetto partecipa a un HashSet o qualcos'altro che si basa su GetHashCode() non sta cambiando?

UPDATE

Proprio imbattuto in questo in MSDN

In genere, si implementa l'uguaglianza valore quando gli oggetti del tipo sono dovrebbero essere aggiunto a una raccolta di qualche tipo, o quando il loro lo scopo principale è quello di memorizzare un insieme di campi o proprietà. È possibile basare la definizione di uguaglianza valore su un confronto di tutti i campi e le proprietà di nel tipo oppure basare la definizione su un sottoinsieme . Ma in entrambi i casi, e in entrambe le classi e le strutture, l'implementazione dovrebbe seguire le cinque garanzie di equivalenza:

+4

Io non credo che sia ragionevole per 'ImaginaryNumber' essere un tipo mutabile. –

+2

In realtà, dovresti probabilmente implementare ImaginaryNumber come un tipo di valore immutabile (struct). – Joe

+0

Davvero? Perché? Tutti gli altri numeri sono mutabili. –

risposta

11

Mi sono reso conto che volevo che Equals significasse due cose diverse, a seconda del contesto. Dopo aver soppesato l'ingresso anche qui, come here, ho optato per il seguente per la mia situazione particolare:

Non sto ignorando Equals() e GetHashCode(), ma preservando il comune, ma per nulla convenzione onnipresenti che Equals() significa uguaglianza identità per le classi e che Equals() indica l'uguaglianza dei valori per le strutture. Il più grande driver di questa decisione è il comportamento degli oggetti nelle raccolte con hash (Dictionary<T,U>, HashSet<T>, ...) se mi allontano da questa convenzione.

Questa decisione mi ha lasciato ancora manca il concetto di uguaglianza valore (come discussed on MSDN)

Quando si definisce una classe o struct, a decidere se ha senso per creare una definizione personalizzata di uguaglianza valore (o equivalenza) per il tipo. In genere, si implementa l'uguaglianza di valore quando si prevede che gli oggetti del tipo vengano aggiunti a una raccolta di qualche tipo o quando lo loro scopo principale è quello di memorizzare un insieme di campi o proprietà.

Un caso tipico per desiderare il concetto di uguaglianza di valore (o come lo sto definendo "equivalenza") è in unit test.

Dato

public class A 
{ 
    int P1 { get; set; } 
    int P2 { get; set; } 
} 

[TestMethod()] 
public void ATest() 
{ 
    A expected = new A() {42, 99}; 
    A actual = SomeMethodThatReturnsAnA(); 
    Assert.AreEqual(expected, actual); 
} 

il test fallisce perchè Equals() sta testando uguaglianza riferimento.

Il test dell'unità può essere certamente modificato per testare ciascuna proprietà singolarmente, ma questo sposta il concetto di equivalenza dalla classe nel codice di test per la classe.

Per mantenere tale conoscenza incapsulato nella classe, e per fornire un quadro coerente per i test di equivalenza, ho definito un'interfaccia che i miei oggetti implementano

public interface IEquivalence<T> 
{ 
    bool IsEquivalentTo(T other); 
} 

l'attuazione di norma segue questo schema:

public bool IsEquivalentTo(A other) 
{ 
    if (object.ReferenceEquals(this, other)) return true; 

    if (other == null) return false; 

    bool baseEquivalent = base.IsEquivalentTo((SBase)other); 

    return (baseEquivalent && this.P1 == other.P1 && this.P2 == other.P2); 
} 

Certamente, se avessi abbastanza classi con proprietà sufficienti, potrei scrivere un helper che costruisca un albero di espressioni tramite la riflessione per implementare IsEquivalentTo().

Infine, ho implementato un metodo di estensione che verifica l'equivalenza delle due IEnumerable<T>:

static public bool IsEquivalentTo<T> 
    (this IEnumerable<T> first, IEnumerable<T> second) 

If T attrezzi IEquivalence<T> tale interfaccia è utilizzata, tuttavia Equals() viene utilizzato, per confrontare elementi della sequenza. Consentendo il fallback a Equals() lascia che funzioni, ad es. con ObservableCollection<string> oltre ai miei oggetti business.

Ora, l'affermazione nel mio test di unità è

Assert.IsTrue(expected.IsEquivalentTo(actual)); 
+0

questo è molto più pigro che pensare attraverso la generazione di hashcode e preoccuparsi di collisioni ed effetti collaterali e di bla bla bla. e questa è una buona cosa :) –

+1

Vorrei che .NET avesse definito due serie di test di uguaglianza virtuale (o incluso un parametro per indicare quale tipo di uguaglianza dovrebbe essere testato). Avere un oggetto incapsulare i dati tenendo un riferimento di tipo mutabile a un'istanza di oggetto che non sarà mai esposto a qualcosa che potrebbe mutare è un modello molto comune; l'oggetto al quale viene tenuto il riferimento dovrebbe fornire un test di uguaglianza che sarebbe significativo in tale scenario. – supercat

+0

@supercat ed EricJ Sono d'accordo. Sembra essere una caratteristica mancante in .Net. – ToolmakerSteve

11

La documentazione MSDN di non sovraccaricare == per i tipi mutabili è sbagliato. Non c'è assolutamente nulla di sbagliato per i tipi mutabili per implementare la semantica dell'uguaglianza. Due articoli possono essere uguali ora anche se cambieranno in futuro.

I pericoli attorno ai tipi mutabili e all'uguaglianza si presentano generalmente quando sono utilizzati come chiave in una tabella hash o consentono ai membri mutabili di partecipare alla funzione GetHashCode.

+0

Poiché '==' rappresenta in realtà due operatori diversi in C#, è discutibile che qualsiasi tipo di classe debba sovraccaricarlo, poiché potrebbe non essere sempre chiaro se rappresenti un test di uguaglianza di riferimento o una chiamata a un sovraccarico '=='.Vorrei inoltre suggerire che l'unica nozione di uguaglianza che è universalmente applicabile a tutti gli oggetti è l'equivalenza, e che gli oggetti di classe distinti possono essere equivalenti solo se non ci sono mezzi con cui potrebbero mai differire. Mentre alcuni tipi "mutabili" soddisfano questo criterio, le cose che possono essere mutate in modo indipendente non lo fanno. – supercat

+0

"Due articoli possono essere uguali anche se cambieranno in futuro." Il problema è che se si cambia == per un tipo mutabile, nessuno può tranquillamente usare un oggetto di quel tipo come chiave del dizionario. Questo è un grosso problema. Mi ha morso, anche per i tipi che ho scritto io, quindi dovrei sapere meglio. Questo è un grave errore di progettazione di Microsoft. L'unica risposta sicura per i mutabili, come EricJ e Supercat discutono in altre risposte, è di lasciare solo Equals e definire la propria funzione di uguaglianza, con un nome diverso. Fastidioso, perché ora devi usare una funzione diversa quando confronti i tuoi oggetti. – ToolmakerSteve

0

Non capisco le vostre preoccupazioni su GetHashCode per quanto riguarda HashSet. GetHashCode restituisce appena un numero che consente di memorizzare internamente e valori di ricerca HashSet. Se il codice hash di un oggetto cambia, l'oggetto non viene rimosso da HashSet, ma non verrà memorizzato nella posizione ottimale.

EDIT

Grazie a @Erik J vedo il punto.

Il HashSet<T> è una raccolta di prestazioni e per raggiungere tale prestazione si basa completamente su GetHashCode essendo costante per la vita della collezione. Se vuoi questo spettacolo, allora devi seguire queste regole. Se può non allora si dovrà passare a qualcos'altro come List<T>

+0

Se si modifica il codice hash di un oggetto dopo che è stato aggiunto a HashSet, 'myHashSet.Contains (myMutatedObject)' restituisce false. Tuttavia, lo stesso HashSet contiene ancora l'oggetto (ora mutato). Quindi, consentendo al valore hash di cambiare si interrompe il contratto HashSet (interruzioni 'Contains'). –

+0

Vedo il punto ora, grazie! –

6

Scopri i Guidelines and rules for GetHashCode da Eric Lippert.

Regola: il numero intero restituito da GetHashCode deve mai cambiare mentre l'oggetto è contenuto in una struttura di dati che dipende dal codice hash rimasto stabile

È consentito, però pericoloso, per rendere un oggetto la cui hash il valore del codice può variare a mano a mano che i campi dell'oggetto mutano.

+1

L'avevo già letto in precedenza, e vale la pena leggerlo per coloro che non lo hanno fatto. In realtà penso che questa linea risponda meglio alla mia domanda: 'Se hai un tale oggetto e lo metti in una tabella hash, allora il codice che muta l'oggetto e il codice che mantiene la tabella hash devono avere un protocollo concordato che assicura che l'oggetto non sia mutato mentre si trova nella tabella hash. Quello che sembra il protocollo dipende da voi. –