Я играю с 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?
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
.
Что делать, если я хочу получить отдельный список, основанный на одном или нескольких свойствах?
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();
Вы также можете использовать синтаксис запроса, если хотите, чтобы он выглядел как 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();
Я думаю, этого достаточно:
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());
Следующий код функционально эквивалентен ответ 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.
Я написал статью, в которой объясняется, как расширить функцию 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 - Указание свойства в отдельной функции
Сначала выберите первую группу по вашим полям, затем выберите элемент firstordefault.
List<Person> distinctPeople = allPeople
.GroupBy(p => p.PersonId)
.Select(g => g.FirstOrDefault())
.ToList();
Если вам нужен метод Distinct для нескольких свойств, вы можете проверить мою библиотеку 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.
Если вы не хотите добавлять библиотеку 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 для фильтрации любых дубликатов.
List<Person>lst=new List<Person>
var result1 = lst.OrderByDescending(a => a.ID).Select(a =>new Player {ID=a.ID,Name=a.Name} ).Distinct();
Лучший способ сделать это, который будет совместим с другими версиями .NET, - это переопределить Equals и GetHash, чтобы справиться с этим (см. вопрос Этот код возвращает различные значения. я хочу вернуть строго типизированную коллекцию, а не анонимный тип), но если вам нужно что-то общее в вашем коде, решения в этой статье великолепны.
Вы должны уметь переопределять Equals на человеке, чтобы на самом деле сделать Equals on Person.id. Это должно привести к поведению, которое вы после.