Distinct () LINQ для определенного свойства

Я играю с LINQ, чтобы узнать об этом, но я не могу понять, как использовать Distinct, когда у меня нет простого списка (простой список целых чисел довольно легко сделать, это не вопрос). Что я, если хочу использовать Distinct в списке объектов на один или больше свойства объекта?

пример: если объект Person, в собственность Id. Как я могу получить все лицо и использовать Distinct на них со свойством Id объекта?

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

как я могу получить только Person1 и Person3? Это возможно?

если это невозможно с LINQ, каков был бы лучший способ иметь список Person в зависимости от некоторых его свойств в .NET 3.5?

19 ответов


редактировать: теперь это часть MoreLINQ.

то, что вам нужно,-это "отличное от" эффективно. Я не верю, что это часть LINQ, как она есть, хотя это довольно легко написать:

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;
        }
    }
}

таким образом, чтобы найти различные значения, используя только Id собственность, вы можете использовать:

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

и для использования нескольких свойств можно использовать анонимные типы, реализующие равенство соответственно:

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

непроверенный, но он должен работать (и теперь он по крайней мере компилируется).

Он предполагает компаратор по умолчанию для ключей, хотя - если вы хотите передать компаратор равенства, просто передайте его в HashSet конструктор.


Что делать, если я хочу получить отдельный список на основе один или больше свойства?

просто! Вы хотите сгруппировать их и выбрать победителя из группы.

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

Если вы хотите определить группы по нескольким свойствам, вот как:

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

вы также можете использовать синтаксис запроса, если хотите, чтобы он выглядел как 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();

Я думаю, достаточно:

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

использование:

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 помогает фильтровать записи (может быть более сложным) и groupby и select выполнить определенную функцию.


Вы можете сделать это со стандартным Linq.ToLookup(). Это создаст коллекцию значений для каждого уникального ключа. Просто выберите первый элемент в коллекции

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

решение первая группа по полям, затем выберите элемент firstordefault.

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

следующий код функционально эквивалентен ответ Джона Скита.

протестировано на .NET 4.5, должно работать на любой более ранней версии 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)));
}

Incidentially, проверить последняя версия DistinctBy Джона Скита.cs на Google Code.


Я написал статью, в которой объясняется, как расширить функцию Distinct, чтобы вы могли сделать следующее:

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.

вот статья: расширение LINQ-указание свойства в отдельной функции


Если вам нужен отдельный метод для нескольких свойств, вы можете проверить my PowerfulExtensions библиотека. В настоящее время он находится в очень молодой стадии, но уже вы можете использовать такие методы, как Distinct, Union, Intersect, за исключением любого количества свойств;

вот как вы его используете:

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

вы можете сделать это (хотя и не молниеносно) так:

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

то есть, " выберите всех людей, где нет другого другого человека в списке с тем же идентификатором."

имейте в виду, в вашем примере, что просто выберите человека 3. Я не уверен, как сказать, что вы хотите, из двух предыдущих.


лично я использую следующий класс:

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();
    }
}

затем метод расширения:

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

наконец, целевое использование:

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

преимущество, которое я нашел, используя этот подход, заключается в повторном использовании LambdaEqualityComparer - класс для других методов, которые принимают IEqualityComparer. (О, и я оставляю yield материал для оригинальной реализации LINQ...)


когда мы столкнулись с такой задачей в нашем проекте, мы определили небольшой API для создания компараторов.

Итак, прецедент был таким:

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

и сам API выглядит так:

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;
}

больше деталей на нашем месте:IEqualityComparer в LINQ.


лучший способ сделать это, который будет совместим с другими версиями .NET, - переопределить Equals и GetHash для обработки этого (см. вопрос переполнения стека этот код возвращает различные значения. Однако я хочу вернуть строго типизированную коллекцию, а не анонимный тип), но если вам нужно что-то общее во всем вашем коде, решения в этой статье великолепны.


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

если вы не хотите добавлять библиотеку MoreLinq в свой проект, чтобы получить DistinctBy функциональности, то вы можете получить тот же конечный результат, используя перегрузку LINQ на это Distinct метод, который принимает в качестве


вы должны иметь возможность переопределять Equals на person, чтобы фактически делать Equals на Person.id. Это должно привести к поведению, которое вам нужно.


пожалуйста, попробуйте с кодом ниже.

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

переопределить Equals (object obj) и 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();
    }
}

а затем просто позвоните:

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