Я играю с 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?
РЕДАКТИРОВАТЬ : Теперь это часть 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
конструктору.
Вы можете сделать это (хотя и не молниеносно) примерно так:
people.Where(p => !people.Any(q => (p != q && p.Id == q.Id)));
То есть «выбрать всех людей, у которых в списке нет другого человека с таким же идентификатором».
Имейте в виду, в вашем примере это просто выберет человека 3. Я не уверен, как сказать, что вы хотите, из двух предыдущих.
Вы должны иметь возможность переопределить Equals для человека, чтобы фактически сделать Equals для Person.id. Это должно привести к тому поведению, которое вы преследуете.
Что если я хочу получить отдельный список на основе одного или нескольких свойств?
Просто! Вы хотите сгруппировать их и выбрать победителя из группы.
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();
Single()
и SingleOrDefault()
каждый бросок, когда у источника более одного предмета. В этой операции мы ожидаем, что каждая группа может иметь более одного элемента. В этом отношении First()
предпочтительнее, FirstOrDefault()
потому что в каждой группе должен быть хотя бы один член ... если только вы не используете EntityFramework, который не может понять, что в каждой группе есть хотя бы один член и требования FirstOrDefault()
. FirstOrDefault()
github.com/dotnet/efcore/issues/12088. Я использую 3.1 и получаю сообщения об «невозможности перевода». Я написал статью, в которой объясняется, как расширить функцию 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 - Указание свойства в отдельной функции
Использование:
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:
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();
Следующий код функционально эквивалентен ответу Джона Скита .
Протестировано на .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 .
Если вам нужен метод Distinct для нескольких свойств, вы можете проверить мою библиотеку PowerfulExtensions . В настоящее время он находится на очень молодой стадии, но уже вы можете использовать методы, такие как Distinct, Union, Intersect, Except, для любого количества свойств;
Вот как вы используете это:
using PowerfulExtensions.Linq;
...
var distinct = myArray.Distinct(x => x.A, x => x.B);
Лучший способ сделать это, совместимый с другими версиями .NET, - переопределить Equals и GetHash, чтобы справиться с этим (см. Вопрос переполнения стека. Этот код возвращает различные значения. Однако я хочу вернуть строго типизированную коллекцию, а не анонимный тип ), но если вам нужно что-то общее в вашем коде, решения в этой статье будут хорошими.
Когда мы столкнулись с такой задачей в нашем проекте, мы определили небольшой 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 .
Вы можете сделать это с помощью стандарта Linq.ToLookup()
. Это создаст коллекцию значений для каждого уникального ключа. Просто выберите первый элемент в коллекции
Persons.ToLookup(p => p.Id).Select(coll => coll.First());
Я думаю этого достаточно
list.Select(s => s.MyField).Distinct();
Лично я использую следующий класс:
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 ...)
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
которая принимает 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 для фильтрации любые дубликаты.
Решение сначала сгруппируйте по полям, затем выберите элемент firstordefault.
List<Person> distinctPeople = allPeople
.GroupBy(p => p.PersonId)
.Select(g => g.FirstOrDefault())
.ToList();
Пожалуйста, попробуйте с кодом ниже.
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();
Вы можете использовать DistinctBy () для получения записей Distinct по свойству объекта. Просто добавьте следующее утверждение перед его использованием:
использование Microsoft.Ajax.Utilities;
и затем используйте его следующим образом:
var listToReturn = responseList.DistinctBy(x => x.Index).ToList();
где «Индекс» - это свойство, для которого я хочу, чтобы данные были различны.
Я потратил некоторое время на размышления о том, почему в моем объекте List не было методов «Где, GroupBy и т. Д.». Ответ прост: вам не нужно просто добавлять System.Collections.Generic, вы также должны добавить System.Linq. Это кажется глупым, но для неопытного это может сэкономить много времени:
using System.Collections.Generic;
using System.Linq;
GroupBy
проще. Если вам это нужно в более чем одном месте, гораздо лучше (IMO) инкапсулировать намерение.IQueryable<T>
, я не понимаю, насколько это актуально. Я согласен, что это не подходит для EF и т. Д., Но в LINQ to Objects я думаю, что это больше подходит, чемGroupBy
. Контекст вопроса всегда важен.