Funkcja Distinct () LINQ dla określonej właściwości


1094

Bawię się z LINQ, aby się o tym dowiedzieć, ale nie mogę wymyślić, jak go używać, Distinctgdy nie mam prostej listy (prosta lista liczb całkowitych jest dość łatwa do zrobienia, to nie jest pytanie). Co jeśli chcę użyć Distinct na liście Object na jednej lub więcej właściwości obiektu?

Przykład: Jeśli obiekt ma Personwłaściwość Id. Jak mogę pobrać całą Osobę i używać Distinctna niej właściwości Idobiektu?

Person1: Id=1, Name="Test1"
Person2: Id=1, Name="Test1"
Person3: Id=2, Name="Test2"

Jak mogę dostać sprawiedliwy Person1i Person3? Czy to jest możliwe?

Jeśli nie jest to możliwe z LINQ, jaki byłby najlepszy sposób posiadania listy Personzależnej od niektórych jej właściwości w .NET 3.5?

Odpowiedzi:


1242

EDYCJA : To jest teraz część MoreLINQ .

To, czego potrzebujesz, to skuteczne „wyróżnienie”. Nie wierzę, że jest częścią LINQ w obecnej formie, chociaż dość łatwo jest napisać:

public static IEnumerable<TSource> DistinctBy<TSource, TKey>
    (this IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
{
    HashSet<TKey> seenKeys = new HashSet<TKey>();
    foreach (TSource element in source)
    {
        if (seenKeys.Add(keySelector(element)))
        {
            yield return element;
        }
    }
}

Aby znaleźć odrębne wartości za pomocą samej Idwłaściwości, możesz użyć:

var query = people.DistinctBy(p => p.Id);

Aby użyć wielu właściwości, możesz użyć typów anonimowych, które odpowiednio implementują równość:

var query = people.DistinctBy(p => new { p.Id, p.Name });

Nie przetestowane, ale powinno działać (i teraz przynajmniej kompiluje się).

Zakłada jednak domyślny moduł porównujący klucze - jeśli chcesz przekazać moduł porównujący równość, po prostu przekaż go HashSetkonstruktorowi.



1
@ ashes999: Nie jestem pewien, co masz na myśli. Kod jest obecny w odpowiedzi i w bibliotece - w zależności od tego, czy chętnie przyjmujesz zależność.
Jon Skeet,

10
@ ashes999: Jeśli robisz to tylko w jednym miejscu, to z pewnością korzystanie z niego GroupByjest prostsze. Jeśli potrzebujesz go w więcej niż jednym miejscu, jest o wiele czystsze (IMO), aby zawrzeć intencję.
Jon Skeet

5
@MatthewWhited: Biorąc pod uwagę, że nie ma IQueryable<T>tutaj wzmianki , nie rozumiem, jak to jest istotne. Zgadzam się, że nie byłoby to odpowiednie dla EF itp., Ale myślę, że w LINQ do Objects jest bardziej odpowiednie niż GroupBy. Kontekst pytania jest zawsze ważny.
Jon Skeet

7
Projekt został przeniesiony na github, oto kod DistinctBy: github.com/morelinq/MoreLINQ/blob/master/MoreLinq/DistinctBy.cs
Phate01

1858

Co jeśli chcę uzyskać odrębną listę opartą na jednej lub więcej właściwości?

Prosty! Chcesz je zgrupować i wybrać zwycięzcę z grupy.

List<Person> distinctPeople = allPeople
  .GroupBy(p => p.PersonId)
  .Select(g => g.First())
  .ToList();

Jeśli chcesz zdefiniować grupy dla wielu właściwości, oto jak:

List<Person> distinctPeople = allPeople
  .GroupBy(p => new {p.PersonId, p.FavoriteColor} )
  .Select(g => g.First())
  .ToList();

1
@ErenErsonmez na pewno. W przypadku mojego opublikowanego kodu, jeśli pożądane jest odroczenie wykonania, zrezygnuj z wywołania ToList.
Amy B

5
Bardzo ładna odpowiedź! Naprawdę pomógł mi w Linq-to-Entities kierowany z widoku sql, w którym nie mogłem modyfikować widoku. Musiałem użyć FirstOrDefault () zamiast First () - wszystko jest w porządku.
Alex KeySmith

8
Próbowałem i powinienem zmienić na Select (g => g.FirstOrDefault ())

26
@ChocapicSz Nie. Zarówno Single()i SingleOrDefault()każdy rzut, gdy źródło ma więcej niż jeden przedmiot. W tej operacji oczekujemy, że każda grupa może mieć więcej niż jeden element. W tym przypadku First()jest preferowane, FirstOrDefault()ponieważ każda grupa musi mieć co najmniej jednego członka ... chyba że używasz EntityFramework, który nie może stwierdzić, że każda grupa ma co najmniej jednego członka i wymaga FirstOrDefault().
Amy B,

2
Wygląda na to, że obecnie nie jest obsługiwany w EF Core, nawet przy użyciu FirstOrDefault() github.com/dotnet/efcore/issues/12088 Mam wersję 3.1 i dostaję błędy „nie mogę tłumaczyć”.
Collin M. Barrett

78

Posługiwać się:

List<Person> pList = new List<Person>();
/* Fill list */

var result = pList.Where(p => p.Name != null).GroupBy(p => p.Id).Select(grp => grp.FirstOrDefault());

wherePomaga filtrować wpisy (może być bardziej skomplikowane) i groupbyi selectwykonać odrębną funkcję.


1
Idealny i działa bez rozszerzania Linq lub korzystania z innej zależności.
DavidScherer

77

Możesz także użyć składni zapytania, jeśli chcesz, aby wyglądała jak LINQ:

var uniquePeople = from p in people
                   group p by new {p.ID} //or group by new {p.ID, p.Name, p.Whatever}
                   into mygroup
                   select mygroup.FirstOrDefault();

4
Hmm, moje myśli to zarówno składnia zapytań, jak i płynna składnia API są tak samo jak LINQ, a ich preferencje są lepsze niż tych, których używają ludzie. Sam wolę płynny interfejs API, więc uważam, że bardziej podobny do LINK, ale wydaje mi się, że to subiektywne
Max Carroll

LINQ-Like nie ma nic wspólnego z preferencjami, ponieważ „LINQ-like” ma związek z wyglądaniem jak inny język zapytań osadzony w C #, wolę płynny interfejs pochodzący ze strumieni Java, ale NIE jest podobny do LINQ.
Ryan The Leach

Świetny!! Jesteś moim bohaterem!
Farzin Kanzi

63

Myślę, że to wystarczy:

list.Select(s => s.MyField).Distinct();

43
Co jeśli będzie potrzebował z powrotem swojego pełnego obiektu, a nie tylko tego konkretnego pola?
Festim Cahani,

1
Jaki dokładnie obiekt z kilku obiektów, które mają tę samą wartość właściwości?
donRumatta

40

Najpierw zgrupuj rozwiązanie według pól, a następnie wybierz pierwszy domyślny element.

    List<Person> distinctPeople = allPeople
   .GroupBy(p => p.PersonId)
   .Select(g => g.FirstOrDefault())
   .ToList();

26

Możesz to zrobić ze standardem Linq.ToLookup(). Spowoduje to utworzenie zbioru wartości dla każdego unikalnego klucza. Wystarczy wybrać pierwszy element w kolekcji

Persons.ToLookup(p => p.Id).Select(coll => coll.First());

17

Poniższy kod jest funkcjonalnie równoważny z odpowiedzią Jona Skeeta .

Testowany na .NET 4.5, powinien działać na każdej wcześniejszej wersji LINQ.

public static IEnumerable<TSource> DistinctBy<TSource, TKey>(
  this IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
{
  HashSet<TKey> seenKeys = new HashSet<TKey>();
  return source.Where(element => seenKeys.Add(keySelector(element)));
}

Nawiasem mówiąc, sprawdź najnowszą wersję DistinctBy.cs Jona Skeeta w Google Code .


3
To dało mi „błąd sekwencji nie ma wartości”, ale odpowiedź Skeeta dała prawidłowy wynik.
What Be Cool

10

Napisałem artykuł, który wyjaśnia, jak rozszerzyć funkcję Distinct, abyś mógł wykonać następujące czynności:

var people = new List<Person>();

people.Add(new Person(1, "a", "b"));
people.Add(new Person(2, "c", "d"));
people.Add(new Person(1, "a", "b"));

foreach (var person in people.Distinct(p => p.ID))
    // Do stuff with unique list here.

Oto artykuł: Rozszerzanie LINQ - Określanie właściwości w funkcji Distinct


3
W twoim artykule jest błąd, po Distinct powinien znajdować się <T>: statyczny IEnumerable publiczny <T> Distinct (to ... Także nie wygląda na to, że będzie działał (ładnie) na więcej niż jednej właściwości, tj. Kombinacji pierwszej i nazwiska
wiersz 1

2
+1, drobny błąd nie jest wystarczającym powodem do przegłosowania, że ​​tak głupie często wywoływał literówkę. I jeszcze nie widzę ogólnej funkcji, która będzie działać dla dowolnej liczby właściwości! Mam nadzieję, że downvoter również zanotował każdą odpowiedź w tym wątku. Ale hej, czym jest ten drugi typ będący obiektem? Sprzeciwiam się !
nawfal

4
Twój link jest zepsuty
Tom Lint,

7

Osobiście korzystam z następującej klasy:

public class LambdaEqualityComparer<TSource, TDest> : 
    IEqualityComparer<TSource>
{
    private Func<TSource, TDest> _selector;

    public LambdaEqualityComparer(Func<TSource, TDest> selector)
    {
        _selector = selector;
    }

    public bool Equals(TSource obj, TSource other)
    {
        return _selector(obj).Equals(_selector(other));
    }

    public int GetHashCode(TSource obj)
    {
        return _selector(obj).GetHashCode();
    }
}

Następnie metoda rozszerzenia:

public static IEnumerable<TSource> Distinct<TSource, TCompare>(
    this IEnumerable<TSource> source, Func<TSource, TCompare> selector)
{
    return source.Distinct(new LambdaEqualityComparer<TSource, TCompare>(selector));
}

Wreszcie zamierzone użycie:

var dates = new List<DateTime>() { /* ... */ }
var distinctYears = dates.Distinct(date => date.Year);

Zaletą, którą znalazłem przy użyciu tego podejścia, jest ponowne użycie LambdaEqualityComparerklasy dla innych metod, które akceptują IEqualityComparer. (Och, i pozostawiam te yieldrzeczy oryginalnej implementacji LINQ ...)


5

Jeśli potrzebujesz metody Distinct na wielu właściwościach, możesz sprawdzić moją bibliotekę PowerfulExtensions . Obecnie jest na bardzo młodym etapie, ale już możesz używać metod takich jak Distinct, Union, Intersect, z wyjątkiem dowolnej liczby właściwości;

Oto jak go używasz:

using PowerfulExtensions.Linq;
...
var distinct = myArray.Distinct(x => x.A, x => x.B);

5

Kiedy w naszym projekcie stanęliśmy przed takim zadaniem, zdefiniowaliśmy mały interfejs API do tworzenia komparatorów.

Przypadek użycia wyglądał tak:

var wordComparer = KeyEqualityComparer.Null<Word>().
    ThenBy(item => item.Text).
    ThenBy(item => item.LangID);
...
source.Select(...).Distinct(wordComparer);

A sam interfejs API wygląda następująco:

using System;
using System.Collections;
using System.Collections.Generic;

public static class KeyEqualityComparer
{
    public static IEqualityComparer<T> Null<T>()
    {
        return null;
    }

    public static IEqualityComparer<T> EqualityComparerBy<T, K>(
        this IEnumerable<T> source,
        Func<T, K> keyFunc)
    {
        return new KeyEqualityComparer<T, K>(keyFunc);
    }

    public static KeyEqualityComparer<T, K> ThenBy<T, K>(
        this IEqualityComparer<T> equalityComparer,
        Func<T, K> keyFunc)
    {
        return new KeyEqualityComparer<T, K>(keyFunc, equalityComparer);
    }
}

public struct KeyEqualityComparer<T, K>: IEqualityComparer<T>
{
    public KeyEqualityComparer(
        Func<T, K> keyFunc,
        IEqualityComparer<T> equalityComparer = null)
    {
        KeyFunc = keyFunc;
        EqualityComparer = equalityComparer;
    }

    public bool Equals(T x, T y)
    {
        return ((EqualityComparer == null) || EqualityComparer.Equals(x, y)) &&
                EqualityComparer<K>.Default.Equals(KeyFunc(x), KeyFunc(y));
    }

    public int GetHashCode(T obj)
    {
        var hash = EqualityComparer<K>.Default.GetHashCode(KeyFunc(obj));

        if (EqualityComparer != null)
        {
            var hash2 = EqualityComparer.GetHashCode(obj);

            hash ^= (hash2 << 5) + hash2;
        }

        return hash;
    }

    public readonly Func<T, K> KeyFunc;
    public readonly IEqualityComparer<T> EqualityComparer;
}

Więcej szczegółów znajduje się na naszej stronie: IEqualityComparer w LINQ .


5

Za pomocą DistinctBy () można uzyskać rekordy Distinct według właściwości obiektu. Po prostu dodaj następującą instrukcję przed użyciem:

using Microsoft.Ajax.Utilities;

a następnie użyj go w następujący sposób:

var listToReturn = responseList.DistinctBy(x => x.Index).ToList();

gdzie „Indeks” jest właściwością, dla której chcę, aby dane były odrębne.


4

Możesz to zrobić (choć nie błyskawicznie) tak:

people.Where(p => !people.Any(q => (p != q && p.Id == q.Id)));

To znaczy: „wybierz wszystkie osoby, których nie ma na liście innej osoby o tym samym identyfikatorze”.

Pamiętaj, że w twoim przykładzie wybrałbyś tylko osobę 3. Nie jestem pewien, jak powiedzieć, czego chcesz, z poprzednich dwóch.


4

Jeśli nie chcesz dodawać biblioteki MoreLinq do swojego projektu tylko po to, aby uzyskać DistinctByfunkcjonalność, możesz uzyskać ten sam wynik końcowy, używając przeciążenia metody Linq, Distinctktóra przyjmuje IEqualityComparerargument.

Zaczynasz od utworzenia ogólnej niestandardowej klasy porównującej równość, która używa składni lambda do wykonania niestandardowego porównania dwóch wystąpień klasy ogólnej:

public class CustomEqualityComparer<T> : IEqualityComparer<T>
{
    Func<T, T, bool> _comparison;
    Func<T, int> _hashCodeFactory;

    public CustomEqualityComparer(Func<T, T, bool> comparison, Func<T, int> hashCodeFactory)
    {
        _comparison = comparison;
        _hashCodeFactory = hashCodeFactory;
    }

    public bool Equals(T x, T y)
    {
        return _comparison(x, y);
    }

    public int GetHashCode(T obj)
    {
        return _hashCodeFactory(obj);
    }
}

Następnie w głównym kodzie używasz go w następujący sposób:

Func<Person, Person, bool> areEqual = (p1, p2) => int.Equals(p1.Id, p2.Id);

Func<Person, int> getHashCode = (p) => p.Id.GetHashCode();

var query = people.Distinct(new CustomEqualityComparer<Person>(areEqual, getHashCode));

Voila! :)

Powyższe zakłada, co następuje:

  • Właściwość Person.Idjest typuint
  • peopleZbiór nie zawiera żadnych elementów zerowych

Jeśli kolekcja może zawierać wartości null, po prostu przepisz lambdy, aby sprawdzić, czy null, np .:

Func<Person, Person, bool> areEqual = (p1, p2) => 
{
    return (p1 != null && p2 != null) ? int.Equals(p1.Id, p2.Id) : false;
};

EDYTOWAĆ

To podejście jest podobne do tego w odpowiedzi Władimira Niestierowskiego, ale jest prostsze.

Jest również podobny do tego w odpowiedzi Joela, ale pozwala na złożoną logikę porównań obejmującą wiele właściwości.

Jeśli jednak Twoje obiekty mogą się różnić tylko do tego Idczasu, inny użytkownik udzielił poprawnej odpowiedzi, że wszystko, co musisz zrobić, to zastąpić domyślne implementacje GetHashCode()i Equals()w swojej Personklasie, a następnie użyć Distinct()gotowej metody Linq do filtrowania wszystkie duplikaty.


Chcę uzyskać tylko unikatowe elementy w słowniczku, czy możesz mi pomóc, używam tego kodu, jeśli TempDT nie ma nic, to m_ConcurrentScriptDictionary = TempDT.AsEnumerable.ToDictionary (Function (x) x.SafeField (fldClusterId, NULL_ID_VALUE), Function (y) y.SafeField (fldParamValue11, NULL_ID_VALUE))
RSB


1
List<Person>lst=new List<Person>
        var result1 = lst.OrderByDescending(a => a.ID).Select(a =>new Player {ID=a.ID,Name=a.Name} ).Distinct();

Czy miałeś na myśli Select() new Personzamiast new Player? Fakt, że zamawiasz ID, w żaden sposób nie informuje Distinct()o tym, aby użyć tej właściwości do określenia wyjątkowości, więc to nie zadziała.
BACON

1

Zastąp metody Equals (object obj) i GetHashCode () :

class Person
{
    public int Id { get; set; }
    public int Name { get; set; }

    public override bool Equals(object obj)
    {
        return ((Person)obj).Id == Id;
        // or: 
        // var o = (Person)obj;
        // return o.Id == Id && o.Name == Name;
    }
    public override int GetHashCode()
    {
        return Id.GetHashCode();
    }
}

a następnie wystarczy zadzwonić:

List<Person> distinctList = new[] { person1, person2, person3 }.Distinct().ToList();

Jednak GetHashCode () powinien być bardziej zaawansowany (aby liczyć także Nazwę), moja odpowiedź jest prawdopodobnie najlepsza. W rzeczywistości, aby zarchiwizować logikę docelową, nie trzeba przesłonić GetHashCode (), wystarczy Equals (), ale jeśli potrzebujemy wydajności, musimy ją przesłonić. Wszystkie algorytmy porównania, najpierw sprawdź skrót, a jeśli są równe, wywołaj Equals ().
Oleg Skripnyak

Również w Equals () pierwszym wierszem powinno być „if (! (Obj to Person)) return false”. Ale najlepszą praktyką jest stosowanie osobnego obiektu rzutowanego na typ, na przykład „var o = obj jako Person; if (o == null) return false;” następnie sprawdź równość z bez rzucania
Oleg Skripnyak

1
Zastępowanie równości takiej jak ta nie jest dobrym pomysłem, ponieważ może mieć niezamierzone konsekwencje dla innych programistów oczekujących, że równość osoby zostanie ustalona na więcej niż jednej właściwości.
B2K

0

Powinieneś być w stanie przesłonić Equals na osobę, aby faktycznie zrobić Equals na Person.id. Powinno to spowodować zachowanie, którego szukasz.


-5

Spróbuj z poniższym kodem.

var Item = GetAll().GroupBy(x => x .Id).ToList();

3
Krótka odpowiedź jest mile widziana, jednak nie przyniesie dużej wartości tym ostatnim użytkownikom, którzy próbują zrozumieć, co się dzieje za tym problemem. Poświęć trochę czasu na wyjaśnienie, jaki jest prawdziwy problem i jak go rozwiązać. Dziękuję ~
Hearen,
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.