1334

我正在使用 LINQ 来了解它,但是Distinct当我没有简单的列表时,我不知道如何使用(简单的整数列表很容易做到,这不是问题)。如果想在对象的一个​​或多个属性的对象列表上使用Distinct怎么办?

示例:如果一个对象是Person, 具有 Property Id。如何获取所有 Person 并将Distinct其与对象的属性一起使用Id

Person1: Id=1, Name="Test1"
Person2: Id=1, Name="Test1"
Person3: Id=2, Name="Test2"

我怎样才能得到公正Person1Person3?那可能吗?

如果 LINQ 无法实现,那么Person根据 .NET 3.5 中的某些属性列出列表的最佳方法是什么?

4

23 回答 23

2234

如果我想获得基于一个多个属性的不同列表怎么办?

简单的!您想将它们分组并从组中选出获胜者。

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

于 2009-01-29T14:39:07.640 回答
1451

编辑:这现在是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构造函数。

于 2009-01-28T21:17:13.163 回答
106

采用:

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不同的功能。

于 2012-02-14T12:52:51.957 回答
86

如果你希望它看起来像 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();
于 2012-03-06T18:28:50.040 回答
80

我认为这就足够了:

list.Select(s => s.MyField).Distinct();
于 2015-01-23T14:54:26.763 回答
49

解决方案首先按您的字段分组,然后选择第一个或默认项目。

    List<Person> distinctPeople = allPeople
   .GroupBy(p => p.PersonId)
   .Select(g => g.FirstOrDefault())
   .ToList();
于 2017-07-13T08:33:18.720 回答
32

您可以使用标准来做到这一点Linq.ToLookup()。这将为每个唯一键创建一组值。只需选择集合中的第一项

Persons.ToLookup(p => p.Id).Select(coll => coll.First());
于 2015-01-20T15:01:47.853 回答
20

以下代码在功能上等同于Jon Skeet 的 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)));
}

顺便说一句,在 Google Code 上查看Jon Skeet 的最新版本 DistinctBy.cs

于 2013-02-06T19:56:37.970 回答
12

我写了一篇文章,解释了如何扩展 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.

这是文章(现在在 Web 存档中):扩展 LINQ - 在不同函数中指定属性

于 2009-03-11T12:21:19.053 回答
10

我个人使用以下类:

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

于 2015-10-30T18:59:07.003 回答
10

从 .NET 6 开始,在 Linq 中有使用DistinctBy()扩展的新解决方案,所以我们可以这样做

var distinctPersonsById = personList.DistinctBy(x => x.Id);
于 2021-06-01T12:06:33.197 回答
9

您可以使用 DistinctBy() 通过对象属性获取不同的记录。使用前只需添加以下语句:

使用 Microsoft.Ajax.Utilities;

然后像下面这样使用它:

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

其中“索引”是我希望数据不同的属性。

于 2019-03-27T06:04:44.570 回答
5

你可以这样做(虽然不是闪电般快速)像这样:

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

也就是说,“选择列表中没有其他具有相同 ID 的人的所有人”。

请注意,在您的示例中,这只会选择第 3 个人。我不确定如何从前两个中分辨出您想要哪个。

于 2009-01-28T20:47:38.260 回答
5

如果您需要对多个属性使用 Distinct 方法,可以查看我的PowerfulExtensions库。目前它还处于非常年轻的阶段,但您已经可以在任意数量的属性上使用 Distinct、Union、Intersect、Except 等方法;

这是你如何使用它:

using PowerfulExtensions.Linq;
...
var distinct = myArray.Distinct(x => x.A, x => x.B);
于 2013-08-15T20:20:50.197 回答
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 in LINQ

于 2014-07-10T21:00:08.423 回答
4

如果您不想将 MoreLinq 库添加到您的项目中只是为了获得DistinctBy功能,那么您可以使用接收参数的 LinqDistinct方法的重载来获得相同的最终结果IEqualityComparer

您首先创建一个通用的自定义相等比较器类,该类使用 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集合不包含任何空元素

如果集合可以包含空值,那么只需重写 lambdas 以检查空值,例如:

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

编辑

这种方法类似于 Vladimir Nesterovsky 的答案,但更简单。

它也类似于 Joel 的答案,但允许涉及多个属性的复杂比较逻辑。

但是,如果您的对象只能在那时有所不同,Id那么另一个用户给出了正确的答案,您需要做的就是覆盖您的类中的默认实现,GetHashCode()然后只需使用Linq的开箱即用方法进行过滤出任何重复。Equals()PersonDistinct()

于 2016-08-22T17:45:16.810 回答
3

覆盖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();
于 2018-09-27T20:31:34.757 回答
2

与其他 .NET 版本兼容的最佳方法是覆盖 Equals 和 GetHash 来处理此问题(请参阅堆栈溢出问题此代码返回不同的值。但是,我想要的是返回一个强类型的集合,而不是匿名类型),但是如果您需要在整个代码中通用的东西,那么本文中的解决方案非常棒。

于 2013-10-21T00:47:18.107 回答
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();
于 2016-05-16T10:42:56.880 回答
0

您应该能够覆盖 Person 上的 Equals 以在 Person.id 上实际执行 Equals。这应该会导致您所追求的行为。

于 2009-01-28T20:49:52.810 回答
0

如果你使用旧的 .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);
于 2021-07-28T20:06:04.707 回答
0

可能这会有所帮助,试试这个。使用 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)));
}
于 2021-11-07T09:56:40.620 回答
-4

请尝试使用以下代码。

var Item = GetAll().GroupBy(x => x .Id).ToList();
于 2018-07-16T05:26:30.017 回答