Wyraźnie nie działa z LINQ to Objects

120
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.

To drukuje Jon Skeet dwukrotnie. Czemu? Próbowałem nawet przesłonić metodę Equals w klasie Author. Wciąż Odrębny nie wydaje się działać. czego mi brakuje?

Edycja: dodałem też przeciążenie operatorów == i! =. Wciąż bez pomocy.

 public static bool operator ==(Author a, Author b)
    {
        return true;
    }
    public static bool operator !=(Author a, Author b)
    {
        return false;
    }
Tanmoy
źródło

Odpowiedzi:

159

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

Wystarczy spojrzeć na twoją listę i zobaczyć, że zawiera ona dwa różne obiekty (nie obchodzi go, że mają te same wartości dla pól członkowskich).

Jednym z obejść jest zaimplementowanie interfejsu IEquatable, jak pokazano tutaj .

Jeśli zmodyfikujesz w ten sposób klasę autora, to powinno 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;
    }
}

Wypróbuj jako DotNetFiddle

skalb
źródło
22
IEquatable jest w porządku, ale niekompletny; należy zawsze implementować razem Object.Equals () i Object.GetHashCode (); IEquatable <T> .Equals nie przesłania Object.Equals, więc zakończy się to niepowodzeniem w przypadku porównań z typami innymi niż silnie typowane, co często występuje w strukturach i zawsze w kolekcjach innych niż ogólne.
AndyM
Czy więc lepiej jest użyć zastąpienia Distinct, które przyjmuje IEqualityComparer <T>, 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 zachowywał się normalnie jak normalny obiekt (tj. Tylko równość odwołań), ale sprawdzaj wartości nazw na potrzeby Distinct, użyj IEqualityComparer. Jeśli zawsze chcesz, aby obiekty autora były porównywane na podstawie wartości nazw, zastąp GetHashCode i Equals lub zaimplementuj IEquatable.
Rex M
3
Zaimplementowałem IEquatable(i overrode Equals/ GetHashCode), ale żaden z moich punktów przerwania nie działa w tych metodach na Linq Distinct?
PeterX
2
@PeterX Też to zauważyłem. Miałem punkty przerwania w GetHashCodei Equals, zostały trafione podczas pętli foreach. Dzieje się tak, ponieważ var temp = books.SelectMany(book => book.Authors).Distinct();zwraca an IEnumerable, co oznacza, że ​​żądanie nie jest wykonywane od razu, jest wykonywane tylko wtedy, gdy dane są używane. Jeśli chciałbyś od razu zobaczyć przykład tego odpalenia, dodaj .ToList()po znaku, a .Distinct()zobaczysz punkty przerwania w Equalsi GetHashCodeprzed foreach.
JabberwockyDecompiler
70

Distinct()Równość sprawdza metody referencyjnej dla typów referencyjnych. Oznacza to, że szuka dosłownie tego samego zduplikowanego obiektu, a nie różnych obiektów, które zawierają te same wartości.

Istnieje przeciążenie, które pobiera IEqualityComparer , więc można określić inną logikę do określania, czy dany obiekt jest równy innemu.

Jeśli chcesz, aby Autor zachowywał się normalnie jak normalny obiekt (tj. Tylko równość odwołań), ale dla celów Distinct sprawdzaj 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, zastąp GetHashCode i Equals lub zaimplementuj IEquatable .

Dwa elementy IEqualityComparerinterfejsu to Equalsi GetHashCode. Twoja logika określania, czy dwa Authorobiekty są równe, wygląda na to, że ciągi Imię i Nazwisko 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();
    }
}
Rex M
źródło
1
Dziękuję Ci! Twoja implementacja GetHashCode () pokazała mi, czego mi jeszcze brakuje. Zwracałem {przekazany obiekt} .GetHashCode (), a nie {właściwość używana do porównania} .GetHashCode (). To zrobiło różnicę i wyjaśnia, dlaczego mój nadal zawodzi - dwa różne odwołania miałyby dwa różne kody skrótu.
pelazem
44

Innym rozwiązaniem bez realizacji IEquatable, Equalsi GetHashCodejest użycie LINQs GroupBymetody 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);
}
Jehof
źródło
1
pomogło mi to, biorąc pod uwagę wydajność, czy to działa z taką samą prędkością ?, jak biorąc pod uwagę powyższe metody?
Biswajeet
znacznie przyjemniejsze niż komplikowanie go metodami implementującymi, a jeśli użyjesz EF, deleguje pracę do serwera sql.
Zapnologica
chociaż ta metoda może działać, wystąpi problem z wydajnością ze względu na liczbę grupowanych elementów
Bellash
@Bellash Zrób to, a potem szybko. Jasne, to grupowanie może oznaczać więcej pracy do wykonania. ale czasami implementacja większej ilości, niż chcesz, jest uciążliwa.
Jehof
2
Wolę to rozwiązanie, ale potem używając „nowego” obiektu w grupie: .GroupBy(y => new { y.FirstName, y.LastName })
Dave de Jong
32

Istnieje jeszcze jeden sposób uzyskania odrębnych wartości z listy typów danych zdefiniowanych przez użytkownika:

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

Z pewnością da inny zestaw danych

Ashu_90
źródło
21

Distinct()wykonuje domyślne porównanie równości obiektów w enumerable. Jeśli nie przesłoniłeś Equals()i GetHashCode(), to używa domyślnej implementacji on object, która porównuje odwołania.

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

IEqualityComparerInterfejs jest wygoda, która 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.

AndyM
źródło
Bardzo dziękuję za tę genialną uwagę dotyczącą uczestniczących obiektów.
suhyura
11

Zastąpiłeś Equals (), ale upewnij się, że nadpisałeś również GetHashCode ()

Eric King
źródło
+1 za podkreślenie GetHashCode (). Nie dodawaj podstawowej implementacji HashCode jak w<custom>^base.GetHashCode()
Dani,
8

Powyższe odpowiedzi są błędne !!! Distinct, jak podano w MSDN, zwraca domyślny Equator, który zgodnie z opisem Właściwość Default sprawdza, czy typ T implementuje interfejs System.IEquatable, a jeśli tak, zwraca EqualityComparer, który używa tej implementacji. W przeciwnym razie zwraca EqualityComparer, który używa zastąpień Object.Equals i Object.GetHashCode dostarczone przez T

Co oznacza, że ​​dopóki przewyższasz Równe, nic ci nie jest.

Kod nie działa, ponieważ sprawdzasz firstname == lastname.

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

Alex
źródło
0

Możesz użyć metody rozszerzenia na liście, która sprawdza unikalność na podstawie obliczonego skrótu. Możesz również zmienić metodę rozszerzenia, aby obsługiwała IEnumerable.

Przykład:

public class Employee{
public string Name{get;set;}
public int Age{get;set;}
}

List<Employee> employees = new List<Employee>();
employees.Add(new Employee{Name="XYZ", Age=30});
employees.Add(new Employee{Name="XYZ", Age=30});

employees = employees.Unique(); //Gives list which contains unique objects. 

Metoda rozszerzenia:

    public static class LinqExtension
        {
            public static List<T> Unique<T>(this List<T> input)
            {
                HashSet<string> uniqueHashes = new HashSet<string>();
                List<T> uniqueItems = new List<T>();

                input.ForEach(x =>
                {
                    string hashCode = ComputeHash(x);

                    if (uniqueHashes.Contains(hashCode))
                    {
                        return;
                    }

                    uniqueHashes.Add(hashCode);
                    uniqueItems.Add(x);
                });

                return uniqueItems;
            }

            private static string ComputeHash<T>(T entity)
            {
                System.Security.Cryptography.SHA1CryptoServiceProvider sh = new System.Security.Cryptography.SHA1CryptoServiceProvider();
                string input = JsonConvert.SerializeObject(entity);

                byte[] originalBytes = ASCIIEncoding.Default.GetBytes(input);
                byte[] encodedBytes = sh.ComputeHash(originalBytes);

                return BitConverter.ToString(encodedBytes).Replace("-", "");
            }
chindirala sampath kumar
źródło
-1

Możesz to osiągnąć na dwa sposoby:

1. Możesz zaimplementować interfejs IEquatable zgodnie z metodą Enumerable.Distinct lub możesz zobaczyć odpowiedź @ skalb w tym poście

2. Jeśli twój obiekt nie ma unikalnego klucza, możesz użyć metody GroupBy dla uzyskania listy odrębnych obiektów, musisz zgrupować wszystkie właściwości obiektu, a następnie wybrać pierwszy obiekt.

Na przykład jak poniżej i pracuje dla mnie:

var distinctList= list.GroupBy(x => new {
                            Name= x.Name,
                            Phone= x.Phone,
                            Email= x.Email,
                            Country= x.Country
                        }, y=> y)
                       .Select(x => x.First())
                       .ToList()

Klasa MyObject wygląda następująco:

public class MyClass{
       public string Name{get;set;}
       public string Phone{get;set;}
       public string Email{get;set;}
       public string Country{get;set;}
}

3. Jeśli twój obiekt ma unikalny klucz, możesz go używać tylko w grupie.

Na przykład unikalnym kluczem mojego obiektu jest Id.

var distinctList= list.GroupBy(x =>x.Id)
                      .Select(x => x.First())
                      .ToList()
Ramil Aliyev
źródło