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

674

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

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

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

Как я могу получить только Person1 и Person3? Возможно ли это?

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

Теги:
linq
distinct
.net-3.5

17 ответов

786
Лучший ответ

EDIT: теперь это часть 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.

  • 0
    Это хорошее решение, если вы предполагаете, что при наличии нескольких непонятных значений (как в его примере) вы стремитесь вернуть первое, которое вы видите в перечислении.
  • 0
    Да, именно это я и предполагал исходя из вопроса. Если бы он попросил Person2 и Person3, это было бы сложнее :)
Показать ещё 28 комментариев
1275

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

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

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();
  • 0
    @ErenErsonmez уверен. С моим опубликованным кодом, если требуется отложенное выполнение, отключите вызов ToList.
  • 5
    Очень хороший ответ! Realllllly помог мне в Linq-to-Entities, управляемых из представления SQL, где я не мог изменить представление. Мне нужно было использовать FirstOrDefault (), а не First () - все хорошо.
Показать ещё 9 комментариев
61

Вы также можете использовать синтаксис запроса, если хотите, чтобы он выглядел как LINQ-like:

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();
  • 3
    Хм, я думаю, что и синтаксис запроса, и свободный синтаксис API так же, как LINQ, похожи друг на друга, и его предпочтение тому, какие люди используют. Я сам предпочитаю свободный API, поэтому я бы посчитал его более похожим на LINK, но тогда я думаю, что это субъективно
  • 0
    LINQ-Like не имеет ничего общего с предпочтениями, поскольку «LINQ-like» имеет отношение к тому, как выглядит другой язык запросов, встроенный в C #, я предпочитаю плавный интерфейс, исходящий из потоков Java, но он НЕ LINQ-Like.
35

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

list.Select(s => s.MyField).Distinct();
  • 30
    Что если ему понадобится вернуть свой полный объект, а не только это конкретное поле?
  • 1
    Какой именно объект из нескольких объектов, имеющих одинаковое значение свойства?
27

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

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 выполняет отдельную функцию.

  • 0
    Отлично, и работает без расширения Linq или использования другой зависимости.
13

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

Persons.ToLookup(p => p.Id).Select(coll => coll.First());
  • 0
    Неплохо! Довольно аккуратное и простое решение, которое даже не «хакерское». :-)
  • 0
    Намного лучше ответить и аккуратно
13

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

Протестировано на .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)));
}

Кстати, проверьте Jon Skeet последняя версия DistinctBy.cs в Google Code.

  • 3
    Это дало мне «последовательность не имеет значения ошибки», но ответ Скита дал правильный результат.
9

Я написал статью, в которой объясняется, как расширить функцию 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 - Указание свойства в отдельной функции

  • 3
    В вашей статье есть ошибка, после Distinct должен быть <T>: public static IEnumerable <T> Distinct (this ... Также не похоже, что он будет работать (приятно) над более чем одним свойством, т.е. комбинацией first и фамилии.
  • 2
    +1, незначительная ошибка не является достаточной причиной для понижения голоса, которое просто так глупо, часто вызывало опечатку. И мне еще предстоит увидеть универсальную функцию, которая будет работать для любого количества объектов! Я надеюсь, что downvoter также отклонил все остальные ответы в этой теме. Но эй, что это за объект второго типа? Я протестую !
6

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

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

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

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

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

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

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

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

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

2

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

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

2

Когда мы столкнулись с такой задачей в нашем проекте, мы определили небольшой 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.

1

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

Сначала вы создаете общий пользовательский класс сравнения равенств, который использует синтаксис лямбда для выполнения пользовательского сравнения двух экземпляров родового класса:

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

Затем в вашем основном коде вы используете его так:

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

Вышеприведенное предполагает следующее:

  • Свойство Person.Id имеет тип int
  • Коллекция people не содержит нулевых элементов

Если коллекция может содержать нули, просто перепишите lambdas для проверки нулевого значения, например:

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

ИЗМЕНИТЬ

Этот подход аналогичен такому в ответе Владимира Нестеровского, но проще.

Он также похож на ответ в Joel, но допускает сложную логику сравнения, включающую несколько свойств.

Однако, если ваши объекты могут отличаться только на Id, тогда другой пользователь дал правильный ответ, что все, что вам нужно сделать, это переопределить реализации по умолчанию GetHashCode() и Equals() в вашем классе Person, а затем просто используйте готовый метод Distinct() Linq для фильтрации любых дубликатов.

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

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

0

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

Ещё вопросы

Сообщество Overcoder
Наверх
Меню