LINQ在特定属性上的Distinct()


1094

我正在与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中的某些属性列出内容的最佳方法是什么?

Answers:


1242

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


7
源于

1
@ ashes999:我不确定你的意思。代码出现在答案库中-取决于您是否乐于接受依赖关系。
乔恩·斯基特

10
@ ashes999:如果您只在一个地方进行此操作,那么可以肯定,使用起来GroupBy更简单。如果您需要一个以上的位置,则将意图封装得更加干净(IMO)。
乔恩·斯基特

5
@MatthewWhited:鉴于这里没有提及IQueryable<T>,所以我看不出它的意义。我同意这不适用于EF等,但是在LINQ to Objects中,我认为它比更加合适GroupBy。问题的内容始终很重要。
乔恩·斯基特

7
该项目在github上移动,这是DistinctBy的代码:github.com/morelinq/MoreLINQ/blob/master/MoreLinq/DistinctBy.cs
Phate01 '18

1857

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

简单!您想将他们分组并从分组中选出一名优胜者。

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();

1
@ErenErsonmez肯定。使用我发布的代码,如果需要推迟执行,请不要执行ToList调用。
艾米B

5
很好的答案!Realllllly在无法修改视图的sql视图的帮助下帮助了Linq-to-Entities。我需要使用FirstOrDefault()而不是First()-一切都很好。
Alex KeySmith'5

8
我尝试了它,它应该更改为Select(g => g.FirstOrDefault())

26
@ChocapicSz没有。双方Single()SingleOrDefault()都甩出当源代码中有多个项目。在此操作中,我们预计每个组可能会有一个以上的项目。为此,First()首选方法是,FirstOrDefault()因为每个组必须至少有一个成员...。除非您使用的是EntityFramework,否则无法确定每个组至少有一个成员和需求FirstOrDefault()
艾米·B

2
似乎目前在EF Core中不受支持,即使使用FirstOrDefault() github.com/dotnet/efcore/issues/12088我在3.1上,但出现“无法翻译”错误。
Collin M. Barrett

78

采用:

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帮助你过滤条目(可能更加复杂)和groupbyselect执行不同的功能。


1
完美,并且无需扩展Linq或使用其他依赖项即可工作。
DavidScherer

77

如果希望查询语法看起来像所有的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();

4
嗯,我的想法是查询语法和流利的API语法都与LINQ一样,并且对于人们使用它们的偏好也是如此。我自己更喜欢流畅的API,所以我会考虑更多的LINK-喜欢,但后来我想这是主观的
最大卡罗尔

LINQ-Like与首选项无关,就像“ LINQ-like”与在C#中嵌入另一种查询语言一样,我更喜欢来自Java流的流畅接口,但不是LINQ-Like。
Ryan The Leach

优秀的!!你是我的英雄!
Farzin Kanzi

63

我认为就足够了:

list.Select(s => s.MyField).Distinct();

43
如果他需要他的全部目标,而不仅仅是那个特定领域怎​​么办?
Festim Cahani 2015年

1
具有相同属性值的几个对象中的哪个对象到底是什么?
donRumatta 2015年

40

解决方案首先按您的字段分组,然后选择“ first”或“ default”项目。

    List<Person> distinctPeople = allPeople
   .GroupBy(p => p.PersonId)
   .Select(g => g.FirstOrDefault())
   .ToList();

26

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

Persons.ToLookup(p => p.Id).Select(coll => coll.First());

17

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

偶然地,在Google Code上查看Jon Skeet的DistinctBy.cs最新版本


3
这给了我一个“序列没有值错误”,但是Skeet的答案产生了正确的结果。
什么会酷

10

我写了一篇文章,解释了如何扩展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-在不同函数中指定属性


3
您的文章有错误,在Distinct之后应该有一个<T>:public static IEnumerable <T> Distinct(this ...另外,它看起来也不能(很好地)在一个以上的属性上起作用,即第一个属性的组合名和姓。
ROW1

2
+1是一个小错误,不足以否决投票理由,以至于如此愚蠢,经常给它打错字。而且我还没有看到一个通用函数将对任何数量的属性都起作用!我希望拒绝投票的人也对这个线程中的所有其他答案都拒绝了。但是嘿,第二种对象是什么呢?我反对 !
nawfal 2012年

4
您的链接已断开
Tom Lint

7

我个人使用以下课程:

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


5

如果需要在多个属性上使用Distinct方法,则可以签出我的PowerfulExtensions库。当前它还处于起步阶段,但是您已经可以在多种属性上使用Distinct,Union,Intersect,Except等方法。

这是您的用法:

using PowerfulExtensions.Linq;
...
var distinct = myArray.Distinct(x => x.A, x => x.B);

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;
}

更多详细信息,请访问我们的网站:LINQ中的IEqualityComparer


5

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

使用Microsoft.Ajax.Utilities;

然后像下面这样使用它:

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

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


4

您可以这样做(尽管不是很快):

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

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

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


4

如果您不想仅仅为了获得DistinctBy功能而将MoreLinq库添加到您的项目中,那么可以使用Linq Distinct方法的重载(接受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集合不包含任何null元素

如果集合可以包含null,则只需重写lambda以检查是否为null,例如:

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的方法进行筛选删除所有重复项。


我想仅在字典中获得唯一项,请您帮忙,我正在使用此代码如果TempDT不是全部,则m_ConcurrentScriptDictionary = TempDT.AsEnumerable.ToDictionary(Function(x)x.SafeField(fldClusterId,NULL_ID_VALUE),Function(y) y.SafeField(fldParamValue11,NULL_ID_VALUE))
RSB


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();

您是Select() new Person不是要代替new Player?但是,您所订购的事实ID并不会以某种方式通知Distinct()您使用该属性来确定唯一性,因此这行不通。
培根

1

重写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();

但是GetHashCode()应该更高级(还要计算名称),我认为这个答案可能是最好的。实际上,要归档目标逻辑,不需要重写GetHashCode(),Equals()就足够了,但是如果我们需要性能,就必须重写它。所有比较算法,首先检查哈希,如果它们相等,则调用Equals()。
Oleg Skripnyak,

另外,在Equals()中,第一行应为“ if(!(obj是Person))返回false”。但是最佳实践是使用转换为类型的单独对象,例如“ var o = obj as Person; if(o == null)return false;”。然后检查与邻平等,不受铸造
奥列格Skripnyak

1
像这样覆盖Equals不是一个好主意,因为它可能对其他程序员产生意外的后果,这些程序员期望Person的Equals不仅取决于单个属性。
B2K

0

您应该能够覆盖对人员等于实际上对Person.id等于。这应该导致您追求的行为。


-5

请尝试以下代码。

var Item = GetAll().GroupBy(x => x .Id).ToList();

3
一个简短的答案是受欢迎的,但是对于试图了解问题背后原因的后者用户来说,它并没有太大的价值。请抽出一些时间来解释造成该问题的真正原因是什么以及如何解决。谢谢
〜– Hearen
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.