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

1095

Я играю с 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?

Патрик Дежарден
источник

Ответы:

1250

РЕДАКТИРОВАТЬ : Теперь это часть 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конструктору.

Джон Скит
источник
7
Источник в DistinctBy: code.google.com/p/morelinq/source/browse/MoreLinq/DistinctBy.cs
контанго
1
@ ashes999: Я не уверен, что вы имеете в виду. Код присутствует в ответе и в библиотеке - в зависимости от того, готовы ли вы взять зависимость.
Джон Скит
10
@ ashes999: Если вы делаете это только в одном месте, то, конечно, использовать GroupByпроще. Если вам это нужно в более чем одном месте, гораздо лучше (IMO) инкапсулировать намерение.
Джон Скит
5
@ MatthewWhited: Учитывая, что здесь нет упоминаний IQueryable<T>, я не понимаю, насколько это актуально. Я согласен, что это не подходит для EF и т. Д., Но в LINQ to Objects я думаю, что это больше подходит, чем GroupBy. Контекст вопроса всегда важен.
Джон Скит
7
Проект перенесен на github, вот код DistinctBy: github.com/morelinq/MoreLINQ/blob/master/MoreLinq/DistinctBy.cs
Phate01
1859

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

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

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();
Эми Б
источник
1
@ErenErsonmez уверен. С моим опубликованным кодом, если требуется отложенное выполнение, отключите вызов ToList.
Эми Б
5
Очень хороший ответ! Realllllly помог мне в Linq-to-Entities, управляемом из представления SQL, где я не мог изменить представление. Мне нужно было использовать FirstOrDefault (), а не First () - все хорошо.
Алекс КейСмит
8
Я попробовал это, и это должно измениться на Select (g => g.FirstOrDefault ())
26
@ChocapicSz Нет. Оба Single()и SingleOrDefault()каждый бросок, когда у источника более одного предмета. В этой операции мы ожидаем, что каждая группа может иметь более одного элемента. В этом отношении First()предпочтительнее, FirstOrDefault()потому что в каждой группе должен быть хотя бы один член .... если только вы не используете EntityFramework, который не может понять, что в каждой группе есть хотя бы один член и требования FirstOrDefault().
Эми Б
2
Кажется, в настоящее время не поддерживается в EF Core, даже используя FirstOrDefault() github.com/dotnet/efcore/issues/12088. Я использую 3.1 и получаю сообщения об «невозможности перевода».
Коллин М. Баррет
79

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

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

karcsi
источник
1
Отлично, и работает без расширения Linq или использования другой зависимости.
DavidScherer
77

Вы также можете использовать синтаксис запроса, если хотите, чтобы он выглядел как 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
Хм, я думаю, что и синтаксис запроса, и свободный синтаксис API так же, как LINQ, похожи друг на друга, и его предпочтение по сравнению с теми, которые используют люди. Я сам предпочитаю свободный API, поэтому я бы посчитал его более похожим на LINK, но тогда я думаю, что это субъективно
Макс Кэрролл
LINQ-Like не имеет ничего общего с предпочтениями, поскольку «LINQ-like» имеет отношение к тому, как выглядит другой язык запросов, встроенный в C #, я предпочитаю свободный интерфейс, исходящий из потоков Java, но он НЕ LINQ-Like.
Райан Лич
Отлично!! Ты мой герой!
Фарзин Канзи
63

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

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

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

    List<Person> distinctPeople = allPeople
   .GroupBy(p => p.PersonId)
   .Select(g => g.FirstOrDefault())
   .ToList();
каит беяз
источник
26

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

Persons.ToLookup(p => p.Id).Select(coll => coll.First());
Дэвид Фаландер
источник
17

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

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

Кстати, посмотрите последнюю версию DistinctBy.cs Джона Скита в Google Code .

Контанго
источник
3
Это дало мне «последовательность не имеет значения ошибки», но ответ Skeet дал правильный результат.
Что было бы круто
10

Я написал статью, в которой объясняется, как расширить функцию 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 и фамилии.
row1
2
+1, незначительная ошибка не является достаточной причиной для понижения голоса, которое настолько глупо, что часто вызывает опечатку. И мне еще предстоит увидеть универсальную функцию, которая будет работать для любого количества объектов! Я надеюсь, что downvoter также отклонил все остальные ответы в этой теме. Но эй, что это за объект второго типа ?? Я протестую !
Nawfal
4
Ваша ссылка не работает
Том Линт,
7

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

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

Joel
источник
5

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

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

using PowerfulExtensions.Linq;
...
var distinct = myArray.Distinct(x => x.A, x => x.B);
Анджей Гис
источник
5

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

Владимир Нестеровский
источник
5

Вы можете использовать DistinctBy () для получения записей Distinct по свойству объекта. Просто добавьте следующее утверждение перед его использованием:

использование Microsoft.Ajax.Utilities;

и затем используйте его следующим образом:

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

где «Индекс» - это свойство, для которого я хочу, чтобы данные были различны.

Гарри. Наим
источник
4

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

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

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

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

MQP
источник
4

Если вы не хотите добавлять библиотеку 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));

Вуаля! :)

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

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

Если коллекция может содержать нули, просто перепишите лямбда-выражения для проверки на ноль, например:

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

РЕДАКТИРОВАТЬ

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

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

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

Каспийский канук
источник
Я хочу, чтобы в диктонике были только уникальные предметы. Можете ли вы помочь, я использую этот код? Если TempDT IsNot Nothing, тогда m_ConcurrentScriptDictionary = TempDT.AsEnumerable.ToDictionary (Function (x) x.SafeField (fldClusterId, NULL_ID_VALUE), Function (y) y.SafeField (fldParamValue11, NULL_ID_VALUE))
RSB
2

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

gcoleman0828
источник
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
источник
Вы имели в виду Select() new Personвместо new Player? Тот факт, что вы заказываете по ID, не каким-то образом информирует Distinct()об использовании этого свойства при определении уникальности, поэтому это не сработает.
Бекон
1

Переопределите методы 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();
Вальдемар Галензиновски
источник
Однако GetHashCode () должен быть более продвинутым (чтобы считать также Имя), этот ответ, вероятно, лучше всего, на мой взгляд. На самом деле, чтобы заархивировать целевую логику, нет необходимости переопределять GetHashCode (), достаточно Equals (), но если нам нужна производительность, мы должны переопределить ее. Все сравнительные алгоритмы, сначала проверьте хеш, и если они равны, тогда вызовите Equals ().
Олег Скрипняк
Кроме того, в Equals () первая строка должна быть «if (! (Obj is Person)) return false». Но лучше всего использовать отдельный объект, приведенный к типу, например, «var o = obj as Person; if (o == null) return false;» тогда проверяем равенство с о без кастинга
Олег Скрипняк
1
Подобное переопределение Equals не является хорошей идеей, поскольку это может иметь непреднамеренные последствия для других программистов, ожидающих, что Equality Person будет определяться более чем по одному свойству.
B2K
0

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

GWLlosa
источник
-5

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

var Item = GetAll().GroupBy(x => x .Id).ToList();
Мохамед Хаммам
источник
3
Короткий ответ приветствуется, однако он не принесет большой пользы последним пользователям, которые пытаются понять, что происходит за этой проблемой. Пожалуйста, уделите немного времени, чтобы объяснить, что является реальной причиной проблемы и как ее решить. Спасибо ~
Слушай