2009-09-02 18 views
88
class Program 
{ 
    static void Main(string[] args) 
    { 
     List<Book> books = new List<Book> 
     { 
      new Book 
      { 
       Name="C# in Depth", 
       Authors = new List<Author> 
       { 
        new Author 
        { 
         FirstName = "Jon", LastName="Skeet" 
        }, 
        new Author 
        { 
         FirstName = "Jon", LastName="Skeet" 
        },      
       } 
      }, 
      new Book 
      { 
       Name="LINQ in Action", 
       Authors = new List<Author> 
       { 
        new Author 
        { 
         FirstName = "Fabrice", LastName="Marguerie" 
        }, 
        new Author 
        { 
         FirstName = "Steve", LastName="Eichert" 
        }, 
        new Author 
        { 
         FirstName = "Jim", LastName="Wooley" 
        }, 
       } 
      }, 
     }; 


     var temp = books.SelectMany(book => book.Authors).Distinct(); 
     foreach (var author in temp) 
     { 
      Console.WriteLine(author.FirstName + " " + author.LastName); 
     } 

     Console.Read(); 
    } 

} 
public class Book 
{ 
    public string Name { get; set; } 
    public List<Author> Authors { get; set; } 
} 
public class Author 
{ 
    public string FirstName { get; set; } 
    public string LastName { get; set; } 
    public override bool Equals(object obj) 
    { 
     return true; 
     //if (obj.GetType() != typeof(Author)) return false; 
     //else return ((Author)obj).FirstName == this.FirstName && ((Author)obj).FirstName == this.LastName; 
    } 

} 

Questo è basato su un esempio in "LINQ in azione". Listato 4.16.Distinto non funziona con LINQ to Objects

Questo stampa due volte Jon Skeet. Perché? Ho persino provato a sovrascrivere il metodo Equals nella classe Author. Still Distinct non sembra funzionare. Cosa mi manca?

Modifica: Ho aggiunto anche il sovraccarico dell'operatore == e! =. Ancora nessun aiuto.

public static bool operator ==(Author a, Author b) 
    { 
     return true; 
    } 
    public static bool operator !=(Author a, Author b) 
    { 
     return false; 
    } 

risposta

114

LINQ Distinto non è così intelligente quando si tratta di oggetti personalizzati.

Tutto ciò che fa è guardare la lista e vedere che ha due oggetti diversi (non gli importa che abbiano gli stessi valori per i campi membri).

Una soluzione è implementare l'interfaccia IEquatable come mostrato here.

Se si modifica la classe Autore come dovrebbe funzionare.

public class Author : IEquatable<Author> 
{ 
    public string FirstName { get; set; } 
    public string LastName { get; set; } 

    public bool Equals(Author other) 
    { 
     if (FirstName == other.FirstName && LastName == other.LastName) 
      return true; 

     return false; 
    } 

    public override int GetHashCode() 
    { 
     int hashFirstName = FirstName == null ? 0 : FirstName.GetHashCode(); 
     int hashLastName = LastName == null ? 0 : LastName.GetHashCode(); 

     return hashFirstName^hashLastName; 
    } 
} 

Try it as DotNetFiddle

+11

IEquatable è soddisfacente ma incompleto; dovresti * sempre * implementare Object.Equals() e Object.GetHashCode() insieme; IEquatable . Equals non esegue l'override di Object.Equals, quindi non riuscirà quando si effettuano confronti non fortemente tipizzati, che si verificano spesso nei framework e sempre in raccolte non generiche. – AndyM

+0

Quindi è meglio usare l'override di Distinct che prende IEqualityComparer come suggerito da Rex M? Intendo cosa dovrei fare se non voglio cadere nella trappola. – Tanmoy

+3

@Tanmoy dipende. Se vuoi che l'autore si comporti normalmente come un oggetto normale (vale a dire solo l'uguaglianza di riferimento) ma controlli i valori del nome per lo scopo di Distinct, usa unCompetitore IEquality. Se * sempre * vuoi che gli oggetti Autore siano confrontati in base ai valori del nome, allora sostituisci GetHashCode ed Equals oppure implementa IEquatable. –

54

I Distinct() controlli metodo di riferimento l'uguaglianza per i tipi di riferimento. Ciò significa che sta cercando letteralmente lo stesso oggetto duplicato, non oggetti diversi che contengono gli stessi valori.

C'è un overload che prende un IEqualityComparer, quindi è possibile specificare logica diversa per determinare se un dato oggetto è uguale a un altro.

Se si desidera Autore a comportarsi normalmente come un oggetto normale (vale a dire solo come riferimento l'uguaglianza), ma ai fini della distinta uguaglianza controllo da valori nome, utilizzare un IEqualityComparer. Se si desidera che gli oggetti Autore vengano sempre confrontati in base ai valori del nome, quindi sostituisce GetHashCode ed è uguale a o implementare IEquatable.

I due membri nell'interfaccia IEqualityComparer sono Equals e GetHashCode. La logica per determinare se due oggetti Author sono uguali sembra essere se le stringhe Nome e Cognome sono uguali.

public class AuthorEquals : IEqualityComparer<Author> 
{ 
    public bool Equals(Author left, Author right) 
    { 
     if((object)left == null && (object)right == null) 
     { 
      return true; 
     } 
     if((object)left == null || (object)right == null) 
     { 
      return false; 
     } 
     return left.FirstName == right.FirstName && left.LastName == right.LastName; 
    } 

    public int GetHashCode(Author author) 
    { 
     return (author.FirstName + author.LastName).GetHashCode(); 
    } 
} 
+0

Grazie ! L'implementazione GetHashCode() mi ha mostrato cosa mi mancava ancora. Stavo restituendo {oggetto passato} .GetHashCode(), non {proprietà utilizzata per il confronto} .GetHashCode(). Questo ha fatto la differenza e spiega perché il mio stava ancora fallendo - due riferimenti diversi avrebbero due codici hash diversi. – pelazem

9

Hai sovrascritta Equals(), ma assicuratevi di ignorare anche GetHashCode()

+0

+1 per enfatizzare GetHashCode(). Non aggiungere l'implementazione di base HashCode come in '^base.GetHashCode() ' – Dani

20

Distinct() esegue il confronto di uguaglianza predefinito sugli oggetti nel enumerabile. Se non è stato sostituito da Equals() e GetHashCode(), allora utilizza l'implementazione predefinita su object, che confronta i riferimenti.

La soluzione più semplice è quello di aggiungere un'implementazione corretta di Equals() e GetHashCode() a tutte le classi che partecipano al grafo di oggetti si sta confrontando (cioè Libro e autore).

L'interfaccia IEqualityComparer è una comodità che permette di implementare Equals() e GetHashCode() in una classe separata quando non si ha accesso alle parti interne delle classi è necessario confrontare, o se si sta utilizzando un diverso metodo di confronto .

+0

Grazie mille per questo geniale proposito degli oggetti partecipanti. – suhyura

33

Un'altra soluzione senza implementare IEquatable, Equals e GetHashCode è quello di utilizzare i LINQs metodo GroupBy e per selezionare il primo elemento della IGrouping.

var temp = books.SelectMany(book => book.Authors) 
       .GroupBy (y => y.FirstName + y.LastName) 
       .Select (y => y.First()); 

foreach (var author in temp){ 
    Console.WriteLine(author.FirstName + " " + author.LastName); 
} 
+1

mi ha aiutato, solo considerando le prestazioni, funziona alla stessa velocità ?, considerando i metodi precedenti? – Biswajeet

+0

molto più bello di complicarlo con i metodi di implementazione e, se si utilizza EF, delegherà il lavoro al server sql. – Zapnologica

+0

mentre questo metodo potrebbe funzionare, ci sarà un problema di prestazioni a causa del numero di cose raggruppate – Bellash

5

Le risposte sopra sono errate !!! Distinti come indicato su MSDN restituisce l'equatore predefinito che come indicato La proprietà Default controlla se il tipo T implementa l'interfaccia System.IEquatable e, in tal caso, restituisce un EqualityComparer che utilizza tale implementazione. In caso contrario, restituisce un EqualityComparer che utilizza le sostituzioni di Object.Equals e Object.GetHashCode forniti da T

Il che significa che fino a quando si overide Uguale si sta bene.

Il motivo per cui il codice non funziona è perché si controlla nome == cognome.

vedere https://msdn.microsoft.com/library/bb348436(v=vs.100).aspx e https://msdn.microsoft.com/en-us/library/ms224763(v=vs.100).aspx

7

C'è un altro modo per ottenere valori distinti dalla lista di utenti definito tipo di dati:

YourList.GroupBy(i => i.Id).Select(i => i.First()).ToList(); 

Sicuramente, darà serie distinta di dati