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

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

28.01.2009 20:45:09
21 ОТВЕТ
РЕШЕНИЕ

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

1227
4.11.2015 12:25:47
Contango 6.02.2013 19:46:28
@ ashes999: Я не уверен, что вы имеете в виду. Код присутствует в ответе и в библиотеке - в зависимости от того, готовы ли вы взять зависимость.
Jon Skeet 19.02.2013 17:07:02
@ ashes999: Если вы делаете это только в одном месте, то, конечно, использовать GroupByпроще. Если вам это нужно в более чем одном месте, гораздо лучше (IMO) инкапсулировать намерение.
Jon Skeet 19.02.2013 17:29:54
@ MatthewWhited: Учитывая, что здесь нет упоминаний IQueryable<T>, я не понимаю, насколько это актуально. Я согласен, что это не подходит для EF и т. Д., Но в LINQ to Objects я думаю, что это больше подходит, чем GroupBy. Контекст вопроса всегда важен.
Jon Skeet 22.01.2017 17:10:36
Проект перешел на github, вот код DistinctBy: github.com/morelinq/MoreLINQ/blob/master/MoreLinq/DistinctBy.cs
Phate01 10.03.2018 12:24:43

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

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

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

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

4
28.01.2009 20:47:38

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

0
28.01.2009 20:49:52

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

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

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();
1833
14.01.2020 19:46:58
@ErenErsonmez уверен. С моим опубликованным кодом, если требуется отложенное выполнение, отключите вызов ToList.
Amy B 17.01.2012 12:34:52
Очень хороший ответ! Realllllly помог мне в Linq-to-Entities, управляемом из представления SQL, где я не мог изменить представление. Мне нужно было использовать FirstOrDefault (), а не First () - все хорошо.
Alex KeySmith 16.05.2012 14:14:32
Я попробовал это, и это должно измениться на Select (g => g.FirstOrDefault ())
user585440 6.01.2016 23:38:56
@ChocapicSz Нет. Оба Single()и SingleOrDefault()каждый бросок, когда у источника более одного предмета. В этой операции мы ожидаем, что каждая группа может иметь более одного элемента. В этом отношении First()предпочтительнее, FirstOrDefault()потому что в каждой группе должен быть хотя бы один член ... если только вы не используете EntityFramework, который не может понять, что в каждой группе есть хотя бы один член и требования FirstOrDefault().
Amy B 17.07.2017 13:41:07
Кажется, в настоящее время не поддерживается в EF Core, даже с использованием FirstOrDefault() github.com/dotnet/efcore/issues/12088. Я использую 3.1 и получаю сообщения об «невозможности перевода».
Collin M. Barrett 21.02.2020 14:22:39

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

10
16.01.2016 18:44:32
В вашей статье есть ошибка, после Distinct должен быть <T>: public static IEnumerable <T> Distinct (this ... Также не похоже, что он будет работать (приятно) над более чем одним свойством, т.е. комбинацией first и фамилии.
row1 17.03.2010 10:01:21
+1, незначительная ошибка не является достаточной причиной для понижения голоса, которое настолько глупо, что часто вызывает опечатку. И мне еще предстоит увидеть универсальную функцию, которая будет работать для любого количества объектов! Я надеюсь, что downvoter также отклонил все остальные ответы в этой теме. Но что это за объект второго типа? Я протестую !
nawfal 22.11.2012 12:08:46
Ваша ссылка не работает
Tom Lint 19.04.2019 08:56:53

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

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

77
31.12.2018 02:33:57
Отлично, и работает без расширения Linq или использования другой зависимости.
DavidScherer 11.03.2019 13:19:44

Вы также можете использовать синтаксис запроса, если хотите, чтобы он выглядел как 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();
77
20.01.2017 19:31:59
Хм, я думаю, что и синтаксис запроса, и свободный синтаксис API так же, как LINQ, похожи друг на друга, и его предпочтение по сравнению с теми, которые используют люди. Я сам предпочитаю свободный API, поэтому я бы посчитал его более похожим на LINK, но тогда я думаю, что это субъективно
Max Carroll 5.01.2018 15:57:25
LINQ-Like не имеет ничего общего с предпочтениями, поскольку «LINQ-like» имеет отношение к тому, как выглядит другой язык запросов, встроенный в C #, я предпочитаю свободный интерфейс, исходящий из потоков Java, но он НЕ LINQ-Like.
Ryan The Leach 2.10.2018 04:11:01
Отлично!! Ты мой герой!
Farzin Kanzi 9.10.2019 13:53:06

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

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

17
23.05.2017 12:10:47
Это дало мне «последовательность не имеет значения ошибки», но ответ Скита дал правильный результат.
What Would Be Cool 22.04.2014 23:42:44

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

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

using PowerfulExtensions.Linq;
...
var distinct = myArray.Distinct(x => x.A, x => x.B);
5
16.01.2016 18:51:17

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

2
23.05.2017 12:02:49

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

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

Persons.ToLookup(p => p.Id).Select(coll => coll.First());
26
16.01.2016 19:07:45

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

list.Select(s => s.MyField).Distinct();
63
23.01.2015 14:54:26
Что если ему понадобится вернуть свой полный объект, а не только это конкретное поле?
Festim Cahani 12.08.2015 13:44:56
Какой именно объект из нескольких объектов, имеющих одинаковое значение свойства?
donRumatta 3.09.2015 10:45:14

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

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

7
30.10.2015 18:59:07
List<Person>lst=new List<Person>
        var result1 = lst.OrderByDescending(a => a.ID).Select(a =>new Player {ID=a.ID,Name=a.Name} ).Distinct();
2
16.05.2016 10:42:56

Если вы не хотите добавлять библиотеку 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 для фильтрации любые дубликаты.

4
22.08.2016 18:03:53
Я хочу получить только уникальные элементы в диктонике. Не могли бы вы помочь, я использую этот код? Если 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 7.01.2020 13:42:28

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

    List<Person> distinctPeople = allPeople
   .GroupBy(p => p.PersonId)
   .Select(g => g.FirstOrDefault())
   .ToList();
40
13.07.2017 08:33:18

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

var Item = GetAll().GroupBy(x => x .Id).ToList();
-5
16.07.2018 07:24:26
Короткий ответ приветствуется, однако он не принесет большой пользы последним пользователям, которые пытаются понять, что происходит за этой проблемой. Пожалуйста, уделите немного времени, чтобы объяснить, что является реальной причиной проблемы и как ее решить. Спасибо ~
Hearen 16.07.2018 05:46:09

Переопределите методы 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();
1
27.09.2018 20:31:34
Однако GetHashCode () должен быть более продвинутым (чтобы считать также Имя), этот ответ, вероятно, лучше всего по моему мнению. На самом деле, чтобы заархивировать целевую логику, нет необходимости переопределять GetHashCode (), достаточно Equals (), но если нам нужна производительность, мы должны переопределить ее. Все сравнительные алгоритмы, сначала проверьте хеш, и если они равны, тогда вызовите Equals ().
Oleg Skripnyak 28.10.2018 05:30:11
Кроме того, в Equals () первая строка должна быть «if (! (Obj is Person)) return false». Но лучше всего использовать отдельный объект, приведенный к типу, например, «var o = obj as Person; if (o == null) return false;» тогда проверяем равенство с о без кастинга
Oleg Skripnyak 28.10.2018 05:48:19
Подобное переопределение Equals не является хорошей идеей, поскольку это может иметь непредвиденные последствия для других программистов, ожидающих, что Equality Person будет определяться более чем для одного свойства.
B2K 10.07.2019 21:17:49

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

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

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

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

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

5
27.03.2019 06:04:44

Я потратил некоторое время на размышления о том, почему в моем объекте List не было методов «Где, GroupBy и т. Д.». Ответ прост: вам не нужно просто добавлять System.Collections.Generic, вы также должны добавить System.Linq. Это кажется глупым, но для неопытного это может сэкономить много времени:

using System.Collections.Generic;
using System.Linq;
0
11.03.2020 15:39:25