LINQ ' s Distinct() na określonej własności

Bawię się LINQ, aby się o tym dowiedzieć, ale nie mogę rozgryźć, Jak używać Distinct, gdy nie mam prostej listy (prosta lista liczb całkowitych jest dość łatwa do zrobienia, nie o to chodzi). Co jeśli chcę użyć Distinct na liście obiektu na Jeden lub więcej właściwości obiektu?

Przykład: jeśli obiekt jest Person, z właściwością Id. Jak Mogę uzyskać wszystkie osoby i używać Distinct na nich z własnością Id z obiekt?

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

Jak mogę dostać tylko Person1 i Person3? Czy to możliwe?

Jeśli nie jest to możliwe z LINQ, jaki byłby najlepszy sposób, aby mieć listę Person w zależności od niektórych jego właściwości w. NET 3.5?

Author: Selim Yildiz, 2009-01-28

20 answers

EDIT : to jest teraz część MoreLINQ .

To, czego potrzebujesz, to "different-by" skutecznie. Nie wierzę, że jest to część LINQ w obecnym stanie, chociaż dość łatwo go 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;
        }
    }
}

Więc aby znaleźć różne wartości używając tylko właściwości Id, możesz użyć:

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

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

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

Nieprzetestowane, ale powinno zadziałać (a teraz przynajmniej kompilacji).

Zakłada domyślne porównywanie kluczy - jeśli chcesz przekazać porównywanie równości, po prostu przekaż je konstruktorowi HashSet.

 1357
Author: Jon Skeet,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-11-04 12:25:47

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

Proste! 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 na wielu właściwościach, oto jak to zrobić:

List<Person> distinctPeople = allPeople
  .GroupBy(p => new {p.PersonId, p.FavoriteColor} )
  .Select(g => g.First())
  .ToList();
 2021
Author: Amy B,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2020-01-14 19:46:58

Użycie:

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

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

where pomaga filtrować wpisy (może być bardziej złożone), a groupby i select wykonują odrębną funkcję.

 93
Author: karcsi,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-12-31 02:33:57

Możesz również użyć składni zapytań, 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();
 80
Author: Chuck Rostance,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-01-20 19:31:59

Myślę, że wystarczy:

list.Select(s => s.MyField).Distinct();
 72
Author: Ivan,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-01-23 14:54:26

Rozwiązanie pierwsza grupa przez pola następnie wybierz firstordefault item.

    List<Person> distinctPeople = allPeople
   .GroupBy(p => p.PersonId)
   .Select(g => g.FirstOrDefault())
   .ToList();
 45
Author: cahit beyaz,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-07-13 08:33:18

Możesz to zrobić ze standardem Linq.ToLookup(). Utworzy to zbiór wartości dla każdego unikalnego klucza. Po prostu wybierz pierwszy element w kolekcji

Persons.ToLookup(p => p.Id).Select(coll => coll.First());
 28
Author: David Fahlander,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-01-16 19:07:45

Poniższy kod jest funkcjonalnie równoważny Odpowiedzi Jona Skeeta .

Testowany na. Net 4.5, powinien działać na wcześniejszych wersjach 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)));
}
/ Align = "center" bgcolor = "# e0ffe0 " / premier Wysp Owczych / / align = center / cs na Google Code .
 17
Author: Contango,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:10:47

Napisałem artykuł, który wyjaśnia, jak rozszerzyć odrębną funkcję, aby można było to zrobić w następujący sposób:

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ł (teraz w archiwum www): rozszerzenie LINQ-określenie właściwości w funkcji Distinct

 11
Author: Timothy Khouri,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2020-09-11 19:14:16

Osobiście używam 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ą tego podejścia jest ponowne użycie klasy LambdaEqualityComparer dla innych metod, które akceptują IEqualityComparer. (Oh, i zostawiam yield rzeczy do oryginalnej implementacji LINQ...)

 8
Author: Joel,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-10-30 18:59:07

Możesz użyć DistinctBy () do uzyskania odrębnych rekordów przez właściwość obiektu. Wystarczy dodać następującą instrukcję przed jej użyciem:

Za Pomocą Microsoftu.Ajax.Utilities;

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

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

Gdzie 'Index' jest właściwością, na której chcę, aby dane były odrębne.

 8
Author: Harry .Naeem,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2019-03-27 06:04:44

Możesz to zrobić (choć nie błyskawicznie) w ten sposób:

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

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

Pamiętaj, że w twoim przykładzie wystarczy wybrać osobę 3. Nie wiem, jak powiedzieć, co chcesz, z poprzednich dwóch.

 5
Author: mqp,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-01-28 20:47:38

W przypadku, gdy potrzebujesz odrębnej metody na wielu właściwościach, możesz sprawdzić moją bibliotekę PowerfulExtensions . Obecnie jest to bardzo młody etap, ale już możesz używać metod takich jak Distinct, Union, Intersect, z wyjątkiem dowolnej liczby właściwości; {]}

Tak się go używa:

using PowerfulExtensions.Linq;
...
var distinct = myArray.Distinct(x => x.A, x => x.B);
 5
Author: Andrzej Gis,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-01-16 18:51:17

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

Więc przypadek użycia wyglądał tak:

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

A samo API wygląda tak:

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
Author: Vladimir Nesterovsky,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-01-16 18:55:07

Jeśli nie chcesz dodawać Biblioteki MoreLinq do swojego projektu tylko po to, aby uzyskać funkcjonalność DistinctBy, możesz uzyskać ten sam efekt końcowy, używając przeciążenia metody Distinct Linq, która przyjmuje argument IEqualityComparer.

Zaczynasz od stworzenia klasy porównującej równość, która używa składni lambda do porównywania dwóch instancji klasy generycznej:

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 tak:

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! :)

The powyżej zakłada się, że:

  • właściwość Person.Id jest typu int
  • zbiór people nie zawiera żadnych elementów null

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

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

EDIT

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

Jest również podobny do tego w odpowiedzi Joela, ale pozwala na skomplikowaną logikę porównywania obejmujące wiele właściwości.

Jednakże, jeśli twoje obiekty mogą się różnić tylko o Id, wtedy inny użytkownik dał poprawną odpowiedź, że wszystko, co musisz zrobić, to zastąpić domyślne implementacje GetHashCode() i Equals() w twojej klasie Person, a następnie po prostu użyć out-of-the-box Distinct() metody Linq, aby odfiltrować wszelkie duplikaty.

 4
Author: Caspian Canuck,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-08-22 18:03:53

Najlepszym sposobem, aby to zrobić, który będzie kompatybilny z innymi wersjami. NET jest nadpisanie Equals i GetHash, aby to obsłużyć (zobacz pytanie o przepełnienie stosu Ten kod zwraca różne wartości. Chcę jednak zwrócić mocno wpisaną kolekcję w przeciwieństwie do anonimowego typu), ale jeśli potrzebujesz czegoś, co jest ogólne w całym kodzie, rozwiązania w tym artykule są świetne.

 2
Author: gcoleman0828,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:02:49

Override Equals (object obj) and GetHashCode () metody:

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 potem po prostu zadzwoń:

List<Person> distinctList = new[] { person1, person2, person3 }.Distinct().ToList();
 2
Author: Waldemar Gałęzinowski,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-09-27 20:31:34
List<Person>lst=new List<Person>
        var result1 = lst.OrderByDescending(a => a.ID).Select(a =>new Player {ID=a.ID,Name=a.Name} ).Distinct();
 1
Author: Arindam,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-05-16 10:42:56

Powinieneś być w stanie nadpisać Equals on person to actually do Equals on Person.id to powinno spowodować zachowanie, którego szukasz.

 0
Author: GWLlosa,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-01-28 20:49:52

Wypróbuj poniższy kod.

var Item = GetAll().GroupBy(x => x .Id).ToList();
 -4
Author: Mohamed Hammam,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-07-16 07:24:26