我正在使用 LINQ 来了解它,但是当我没有一个简单的列表时,我不知道如何使用 Distinct
(一个简单的整数列表很容易做到,这不是问题)。如果想在对象的 one 或 more 属性上的对象列表上使用 Distinct 怎么办?
示例:如果对象是 Person
,则具有属性 Id
。如何使用对象的属性 Id
获取所有 Person 并在其上使用 Distinct
?
Person1: Id=1, Name="Test1"
Person2: Id=1, Name="Test1"
Person3: Id=2, Name="Test2"
我怎样才能只得到 Person1
和 Person3
?那可能吗?
如果 LINQ 无法实现,那么根据 .NET 3.5 中的某些属性来获得 Person
列表的最佳方法是什么?
如果我想获得基于一个或多个属性的不同列表怎么办?
简单的!您想将它们分组并从组中选出获胜者。
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();
注意:某些查询提供程序无法解决每个组必须至少有一个元素,并且 First 是在这种情况下调用的适当方法。如果您发现自己在使用这样的查询提供程序,FirstOrDefault 可能会帮助您通过查询提供程序获取查询。
注意 2:考虑使用 EF Core(在 EF Core 6 之前)兼容方法的这个答案。 https://stackoverflow.com/a/66529949/8155
编辑:现在是 MoreLINQ 的一部分。
你需要的是一个有效的“distinct-by”。我不相信它是 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
构造函数。
GroupBy
更简单。如果您在多个地方需要它,那么封装意图会更清洁(IMO)。
IQueryable<T>
,我看不出它有什么相关性。我同意这不适合 EF 等,但在 LINQ to Objects 中,我认为它比GroupBy
更适合。问题的背景总是很重要的。
GroupBy()
/group by
/ToLookup()
答案的解决方案,因为与 Distinct()
一样,它能够yield
遇到一个元素 (第一次),而那些其他方法在整个输入序列被消耗完之前不能返回任何东西。我认为这是一个重要的,呃,区别值得在答案中指出。此外,就内存而言,通过最后一个元素,此 HashSet<>
将仅存储 unique
元素,而其他方法将在某处存储具有 unique + duplicates
元素的 unique
组。
利用:
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();
我认为这就足够了:
list.Select(s => s.MyField).Distinct();
解决方案首先按您的字段分组,然后选择 FirstOrDefault
项。
List<Person> distinctPeople = allPeople
.GroupBy(p => p.PersonId)
.Select(g => g.FirstOrDefault())
.ToList();
您可以使用标准 Linq.ToLookup()
做到这一点。这将为每个唯一键创建一组值。只需选择集合中的第一项
Persons.ToLookup(p => p.Id).Select(coll => coll.First());
从 .NET 6 开始,有使用 the new DistinctBy()
extension in Linq 的新解决方案,所以我们可以这样做:
var distinctPersonsById = personList.DistinctBy(x => x.Id);
DistinctBy
方法的签名:
// Returns distinct elements from a sequence according to a specified
// key selector function.
public static IEnumerable<TSource> DistinctBy<TSource, TKey> (
this IEnumerable<TSource> source,
Func<TSource, TKey> keySelector);
以下代码在功能上等同于 Jon Skeet's answer。
在 .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's latest version of DistinctBy.cs on Google Code。
2022-04-03 更新
根据 Andrew McClement 的评论,最好接受 John Skeet 的回答。
我写了一篇文章,解释了如何扩展 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.
这是文章(现在在网络档案中):Extending LINQ - Specifying a Property in the Distinct Function
我个人使用以下类:
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 实现......)
您可以使用 DistinctBy() 通过对象属性获取不同的记录。使用前只需添加以下语句:
使用 Microsoft.Ajax.Utilities;
然后像下面这样使用它:
var listToReturn = responseList.DistinctBy(x => x.Index).ToList();
其中“索引”是我希望数据不同的属性。
你可以这样做(虽然不是闪电般快速)像这样:
people.Where(p => !people.Any(q => (p != q && p.Id == q.Id)));
也就是说,“选择列表中没有其他具有相同 ID 的人的所有人”。
请注意,在您的示例中,这只会选择第 3 个人。我不确定如何从前两个中分辨出您想要哪个。
如果您需要对多个属性使用 Distinct 方法,可以查看我的 PowerfulExtensions 库。目前它还处于非常年轻的阶段,但您已经可以在任意数量的属性上使用 Distinct、Union、Intersect、Except 等方法;
这是你如何使用它:
using PowerfulExtensions.Linq;
...
var distinct = myArray.Distinct(x => x.A, x => x.B);
当我们在项目中遇到这样的任务时,我们定义了一个小的 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 in LINQ。
如果您不想为了获得 DistinctBy
功能而将 MoreLinq 库添加到您的项目,那么您可以使用接收 IEqualityComparer
参数的 Linq 的 Distinct
方法的重载来获得相同的最终结果。
您首先创建一个通用的自定义相等比较器类,该类使用 lambda 语法来执行通用类的两个实例的自定义比较:
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 集合不包含任何 null 元素
如果集合可以包含空值,那么只需重写 lambdas 以检查空值,例如:
Func<Person, Person, bool> areEqual = (p1, p2) =>
{
return (p1 != null && p2 != null) ? int.Equals(p1.Id, p2.Id) : false;
};
编辑
这种方法类似于 Vladimir Nesterovsky 的答案,但更简单。
它也类似于 Joel 的答案,但允许涉及多个属性的复杂比较逻辑。
但是,如果您的对象只能相差 Id
,那么另一个用户给出了正确答案,您需要做的就是覆盖 Person
类中 GetHashCode()
和 Equals()
的默认实现,然后只需使用Linq 的开箱即用 Distinct()
方法可过滤掉任何重复项。
覆盖 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();
与其他 .NET 版本兼容的最佳方法是覆盖 Equals 和 GetHash 来处理此问题(请参阅 Stack Overflow 问题 This code returns distinct values. However, what I want is to return a strongly typed collection as opposed to an anonymous type),但如果您需要通篇通用的东西您的代码,本文中的解决方案很棒。
List<Person>lst=new List<Person>
var result1 = lst.OrderByDescending(a => a.ID).Select(a =>new Player {ID=a.ID,Name=a.Name} ).Distinct();
Select()
new Person
而不是 new Player
?但是,您按 ID
订购的事实并不会以某种方式通知 Distinct()
使用该属性来确定唯一性,因此这是行不通的。
您应该能够覆盖 Person 上的 Equals 以在 Person.id 上实际执行 Equals。这应该会导致您所追求的行为。
如果你使用旧的 .NET 版本,扩展方法不是内置的,那么你可以定义你自己的扩展方法:
public static class EnumerableExtensions
{
public static IEnumerable<T> DistinctBy<T, TKey>(this IEnumerable<T> enumerable, Func<T, TKey> keySelector)
{
return enumerable.GroupBy(keySelector).Select(grp => grp.First());
}
}
使用示例:
var personsDist = persons.DistinctBy(item => item.Name);
可能这会有所帮助,试试这个。使用 HashSet 的代码性能更高。
public static IEnumerable<TSource> DistinctBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
{
var known = new HashSet<TKey>();
return source.Where(element => known.Add(keySelector(element)));
}
请尝试使用以下代码。
var Item = GetAll().GroupBy(x => x .Id).ToList();
Single()
和SingleOrDefault()
都会抛出。在此操作中,我们预计每个组可能有多个项目的可能性。就此而言,First()
优于FirstOrDefault()
,因为每个组必须至少有一个成员....除非您使用 EntityFramework,它无法确定每个组至少有一个成员并要求 { 4}。FirstOrDefault()
github.com/dotnet/efcore/issues/12088 我在 3.1 上,我得到“无法翻译”错误。