Perché l’implementazione Equals per i tipi anonimi confronta i campi?

Mi chiedo solo perché i progettisti della lingua abbiano deciso di implementare Equals su tipi anonimi in modo simile a Equals sui tipi di valore. Non è fuorviante?

class Person { public string Name { get; set; } public int Age { get; set; } } public static void ProofThatAnonymousTypesEqualsComparesBackingFields() { var personOne = new { Name = "Paweł", Age = 18 }; var personTwo = new { Name = "Paweł", Age = 18 }; Console.WriteLine(personOne == personTwo); // false Console.WriteLine(personOne.Equals(personTwo)); // true Console.WriteLine(Object.ReferenceEquals(personOne, personTwo)); // false var personaOne = new Person { Name = "Paweł", Age = 11 }; var personaTwo = new Person { Name = "Paweł", Age = 11 }; Console.WriteLine(personaOne == personaTwo); // false Console.WriteLine(personaOne.Equals(personaTwo)); // false Console.WriteLine(Object.ReferenceEquals(personaOne, personaTwo)); // false } 

A prima vista, tutti i valori booleani stampati dovrebbero essere falsi. Ma le linee con le chiamate Equals restituiscono valori diversi quando viene utilizzato il tipo Persona e viene utilizzato il tipo anonimo.

Le istanze di tipo anonime sono valori di dati immutabili senza comportamento o id quadro. Non ha molto senso fare riferimento, confrontarli. In quel contesto penso sia del tutto ragionevole generare per loro paragoni di uguaglianza strutturale.

Se si desidera passare il comportamento di confronto a qualcosa di personalizzato (confronto di riferimento o insensibilità alle maiuscole e minuscole), è ansible utilizzare Resharper per convertire il tipo anonimo in una class denominata. Resharper può anche generare membri di uguaglianza.

C’è anche un motivo molto pratico per farlo: i tipi anonimi sono convenienti da usare come chiavi hash nei join e nei raggruppamenti LINQ. Per questo motivo richiedono equazioni semanticamente corrette e implementazioni GetHashCode .

Per la parte del perché dovresti chiedere ai designer di lingue …

Ma ho trovato questo nell’articolo di Eric Lippert sui Tipi anonimi di unificazione in un assemblaggio, seconda parte

Un tipo anonimo ti dà un posto conveniente per memorizzare un piccolo insieme immutabile di coppie nome / valore, ma ti dà di più. Fornisce anche un’implementazione di Equals, GetHashCode e, più pertinente a questa discussione, ToString. (*)

Dove la parte del motivo viene nella nota:

(*) Forniamo Equals e GetHashCode in modo da poter utilizzare le istanze di tipi anonimi nelle query LINQ come chiavi su cui eseguire i join. LINQ to Objects implementa i join usando una tabella hash per motivi di prestazioni, e quindi abbiamo bisogno di implementazioni corrette di Equals e GetHashCode.

La risposta ufficiale dalla specifica del linguaggio C # (disponibile qui ):

I metodi Equals e GetHashcode su tipi anonimi sovrascrivono i metodi ereditati dall’object e sono definiti in termini di Equals e GetHashcode delle proprietà, in modo che due istanze dello stesso tipo anonimo siano uguali se e solo se tutte le loro proprietà sono uguali .

(La mia enfasi)

Le altre risposte spiegano perché questo è fatto.

Vale la pena notare che in VB.Net l’ implementazione è diversa :

Un’istanza di tipi anonimi che non ha proprietà chiave è uguale solo a se stessa.

Le proprietà chiave devono essere indicate esplicitamente quando si crea un object di tipo anonimo. L’impostazione predefinita è: nessuna chiave, che può essere molto confusa per gli utenti di C #!

Questi oggetti non sono uguali in VB, ma sarebbero in codice equivalente a C #:

 Dim prod1 = New With {.Name = "paperclips", .Price = 1.29} Dim prod2 = New With {.Name = "paperclips", .Price = 1.29} 

Questi oggetti valutano “uguale”:

 Dim prod3 = New With {Key .Name = "paperclips", .Price = 1.29} Dim prod4 = New With {Key .Name = "paperclips", .Price = 2.00} 

Perché ci dà qualcosa che è utile. Considera quanto segue:

 var countSameName = from p in PersonInfoStore group p.Id by new {p.FirstName, p.SecondName} into grp select new{grp.Key.FirstName, grp.Key.SecondName, grp.Count()}; 

Funziona perché l’implementazione di Equals() e GetHashCode() per i tipi anonimi funziona sulla base dell’uguaglianza field-by-field.

  1. Ciò significa che quanto sopra sarà più vicino alla stessa query quando viene eseguito contro PersonInfoStore che non è linq-to-objects. (Non è lo stesso, corrisponderà a quello che farà una sorgente XML, ma non a quello che la maggior parte delle collazioni dei database comporterebbe).
  2. Significa che non dobbiamo definire un IEqualityComparer per ogni chiamata a GroupBy che renderebbe il gruppo davvero difficile con oggetti anonimi – è ansible ma non facile definire un IEqualityComparer per oggetti anonimi – e lontano dal significato più naturale.
  3. Soprattutto, non causa problemi nella maggior parte dei casi.

Il terzo punto merita di essere esaminato.

Quando definiamo un tipo di valore, vogliamo naturalmente un concetto di uguaglianza basato sul valore. Anche se potremmo avere un’idea diversa dell’uguaglianza basata sul valore rispetto al valore predefinito, come ad esempio la corrispondenza di un dato campo con insensibilità, l’impostazione predefinita è naturalmente ragionevole (se scarsa in termini di prestazioni e bug in un caso *). (Inoltre, l’uguaglianza di riferimento non ha senso in questo caso).

Quando definiamo un tipo di riferimento, possiamo o non vogliamo un concetto di uguaglianza basato sul valore. L’impostazione predefinita ci fornisce l’uguaglianza di riferimento, ma possiamo facilmente cambiarlo. Se lo cambiamo, possiamo cambiarlo solo per Equals e GetHashCode o per loro e anche == .

Quando definiamo un tipo anonimo, oh aspetta, non lo abbiamo definito, questo è ciò che significa anonimo! La maggior parte degli scenari in cui ci preoccupiamo dell’eguaglianza di riferimento non ci sono più. Se continueremo a tenere in mano un object abbastanza a lungo da chiederci se è uguale a un altro, probabilmente non ci occuperemo di un object anonimo. I casi in cui ci preoccupiamo dell’uguaglianza basata sul valore si presentano molto. Molto spesso con Linq ( GroupBy come abbiamo visto sopra, ma anche Distinct , Union , GroupJoin , Intersect , SequenceEqual , ToDictionary e ToLookup ) e spesso con altri usi (non è come se non stessimo facendo le cose che Linq fa per noi con enumerables in 2.0 e in una certa misura prima di allora, chiunque codificasse in 2.0 avrebbe scritto metà dei metodi in Enumerable ).

In tutto, guadagniamo molto dal modo in cui l’uguaglianza funziona con classi anonime.

Nell’opportunità che qualcuno voglia davvero uguaglianza di riferimento, == usando l’uguaglianza di riferimento significa che lo hanno ancora, quindi non perdiamo nulla. È la strada da percorrere.

* L’implementazione predefinita di Equals() e GetHashCode() ha un’ottimizzazione che consente di utilizzare una corrispondenza binaria nei casi in cui è sicuro farlo. Sfortunatamente c’è un bug che a volte rende errato l’identificazione di alcuni casi come sicuri per questo approccio più veloce quando non lo sono (o almeno lo ha usato, forse è stato corretto). Un caso comune è se si dispone di un campo decimal , in una struttura, quindi considererà alcune istanze con campi equivalenti come non uguali.