2009-09-02 26 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; 
    } 

} 

Jest to oparte na przykładzie w "LINQ w akcji". Listing 4.16.Wyraźne nie działa z LINQ do obiektów

To drukuje Jon Skeet dwa razy. Czemu? Próbowałem nawet przesłonić metodę Equals w klasie autora. Still Distinct nie działa. czego mi brakuje?

Edytuj: Dodałem też przeciążenie operatora == i! =. Nadal nie ma pomocy.

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

Odpowiedz

114

LINQ Distinct nie jest tak inteligentny, jeśli chodzi o niestandardowe obiekty.

Wystarczy spojrzeć na listę i zobaczyć, że ma dwa różne obiekty (nie obchodzi ich, że mają te same wartości dla pól elementów).

Jednym z rozwiązań jest wdrożenie interfejsu IEquatable, jak pokazano na rysunku here.

Jeśli zmodyfikujesz klasę Autor, tak jak powinna, powinna działać.

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 jest w porządku, ale niekompletne; powinieneś * zawsze * wdrażać razem Object.Equals() i Object.GetHashCode(); IEquatable .Equals nie zastępuje Object.Equals, więc to się nie powiedzie podczas wykonywania porównań niesfornych, które występują często w frameworkach i zawsze w kolekcjach nie generycznych. – AndyM

+0

Więc lepiej jest użyć zastąpienia Distinct, które ma IEqualityComparer , jak zasugerował Rex M? Mam na myśli to, co powinienem robić, jeśli nie chcę wpaść w pułapkę. – Tanmoy

+3

@Tanmoy to zależy. Jeśli chcesz, aby autor normalnie zachowywał się jak normalny obiekt (to jest tylko referencyjna równość), ale sprawdzaj wartości nazw dla celu Distinct, użyj IEqualityComparer. Jeśli * zawsze * chcesz, aby obiekty Autora były porównywane na podstawie wartości nazw, to nadpisuj GetHashCode i Equals lub implementuj IEquatable. –

54

Metoda Distinct() sprawdza równanie odniesienia dla typów odniesienia. Oznacza to, że szuka on dosłownie tego samego obiektu, który jest duplikowany, a nie różnych obiektów, które zawierają te same wartości.

Istnieje overload, który pobiera IEqualityComparer, dzięki czemu można określić inną logikę dla określenia, czy dany obiekt jest równy drugiemu.

Jeśli chcesz, aby autor normalnie zachowywał się jak normalny obiekt (to jest tylko referencyjna równość), ale dla celów Wyróżnij sprawdź równość według wartości nazw, użyj IEqualityComparer. Jeśli zawsze chcesz, aby obiekty Autora były porównywane na podstawie wartości nazw, wówczas przesłaniają GetHashCode i Equals implementują IEquatable.

Dwóch członków interfejsu IEqualityComparer to Equals i GetHashCode. Twoja logika określania, czy dwa obiekty Author są równe, wydaje się być taka, jeśli ciągi Pierwszy i Ostatni są takie same.

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

Dziękuję ! Twoja implementacja GetHashCode() pokazała mi, czego jeszcze mi brakowało. Wracałem {przekazany obiekt} .GetHashCode(), nie {właściwość używana do porównania} .GetHashCode(). To zrobiło różnicę i wyjaśnia, dlaczego mój wciąż nie działa - dwa różne odniesienia miałyby dwa różne kody skrótu. – pelazem

9

pan nadpisane equals(), ale upewnij się, że również zastąpić GetHashCode()

+0

+1 za podkreślenie GetHashCode(). Nie dodawaj podstawowej implementacji HashCode, jak w '^base.GetHashCode() ' – Dani

20

Distinct() wykonuje domyślny porównania równości na obiektach w przeliczalny. Jeśli nie zastąpiłeś Equals() i GetHashCode(), to używa domyślnej implementacji na object, która porównuje odniesienia.

Prostym rozwiązaniem jest dodanie prawidłowe realizację Equals() i GetHashCode() do wszystkich klas, które uczestniczą w wykresie obiektu porównujesz (tj książkę i autora).

Interfejs IEqualityComparer to udogodnienie, które pozwala na wdrożenie Equals() i GetHashCode() w oddzielnej klasie, gdy nie masz dostępu do wewnętrznych części zajęć trzeba porównać, lub jeśli używasz innej metody porównywania .

+0

Dziękuję bardzo za błyskotliwą komendę na temat uczestniczących obiektów. – suhyura

33

Innym rozwiązaniem bez wdrożenia IEquatable, Equals i GetHashCode jest użycie LINQs GroupBy sposobu i wybierz pierwszą pozycję z 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

Pomogło mi to, po prostu biorąc pod uwagę wydajność, czy to działa z tą samą prędkością ?, jak rozważając powyższe metody? – Biswajeet

+0

o wiele ładniejszy niż komplikując go metodami implementacji, a użycie EF spowoduje przeniesienie pracy do serwera sql. – Zapnologica

+0

podczas gdy ta metoda może działać, wystąpi problem z wydajnością ze względu na liczbę elementów zgrupowanych – Bellash

5

Powyższe odpowiedzi są błędne !!! W odróżnieniu od tego, co podano w MSDN, zwraca domyślny równik, który zgodnie z instrukcją Właściwość Default sprawdza, czy typ T implementuje interfejs System.nyquatable, a jeśli tak, zwraca EqualityComparer, który używa tej implementacji. W przeciwnym razie, zwraca EqualityComparer który używa nadpisania z Object.Equals i Object.GetHashCode świadczonych przez T

Czyli jak długo overide Równa jesteś w porządku.

Powodem, dla którego kod nie działa, jest to, że sprawdzasz imię == nazwisko.

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

7

Jest jeszcze jeden sposób, aby uzyskać różne wartości z listy zdefiniowanych przez użytkownika typ danych:

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

pewno, to daje wyraźny zestaw danych