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?

Patrick Desjardins
źródło

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.

Jon Skeet
źródło
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();
Amy B.
źródło
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ę.

karcsi
źródło
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();
Chuck Rostance
źródło
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();
Ivan
źródło
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();
Cahit Beyaz
źródło
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());
David Fahlander
źródło
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 .

Contango
źródło
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

Timothy Khouri
źródło
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 ...)

Joel
źródło
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);
Andrzej Gis
źródło
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 .

Władimir Niestierowski
źródło
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.

Harry .Naeem
źródło
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.

mqp
źródło
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.

Kaspijski Canuck
źródło
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
2

Najlepszym sposobem na zrobienie tego, który będzie kompatybilny z innymi wersjami platformy .NET, jest zastąpienie Equals i GetHash, aby sobie z tym poradzić (patrz Pytanie o przepełnieniu stosu Ten kod zwraca różne wartości. Chcę jednak zwrócić silnie typowaną kolekcję w przeciwieństwie do anonimowy typ ), ale jeśli potrzebujesz czegoś, co jest ogólne w całym kodzie, rozwiązania w tym artykule są świetne.

gcoleman0828
źródło
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();
Arindam
źródło
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();
Waldemar Gałęzinowski
źródło
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.

GWLlosa
źródło
-5

Spróbuj z poniższym kodem.

var Item = GetAll().GroupBy(x => x .Id).ToList();
Mohamed Hammam
źródło
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,