【问题标题】:LINQ's Distinct() on a particular propertyLINQ 在特定属性上的 Distinct()
【发布时间】:2010-10-04 02:33:44
【问题描述】:

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

示例:如果一个对象是Person,其属性为Id。如何获取所有 Person 并在其上使用 Distinct 以及对象的属性 Id

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

我怎样才能只得到Person1Person3?这可能吗?

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

【问题讨论】:

    标签: c# linq .net-3.5 distinct


    【解决方案1】:

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

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

    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

    【讨论】:

    • @ErenErsonmez 当然。使用我发布的代码,如果需要延迟执行,请停止 ToList 调用。
    • 非常好的答案! Realllllly 在我无法修改视图的 sql 视图驱动的 Linq-to-Entities 中帮助了我。我需要使用 FirstOrDefault() 而不是 First() - 一切都很好。
    • 我试过了,应该改成 Select(g => g.FirstOrDefault())
    • @ChocapicSz 不。 Single()SingleOrDefault() 在源有多个项目时都会抛出。在此操作中,我们预计每个组可能有多个项目的可能性。就此而言,First() 优于 FirstOrDefault(),因为每个组必须至少有一个成员.... 987654329@.
    • 目前似乎不支持 EF Core,即使使用 FirstOrDefault() github.com/dotnet/efcore/issues/12088 我在 3.1 上,出现“无法翻译”错误。
    【解决方案2】:

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

    【讨论】:

    • @ashes999:我不确定你的意思。代码存在于库中的答案 中 - 取决于您是否愿意承担依赖项。
    • @ashes999:如果您只在一个地方执行此操作,那么可以肯定,使用GroupBy 会更简单。如果您在多个地方需要它,那么封装意图会更清晰(IMO)。
    • @MatthewWhited:鉴于这里没有提到IQueryable&lt;T&gt;,我看不出它有什么关系。我同意这不适合 EF 等,但在 LINQ to Objects 中,我认为它比GroupBy 更适合。问题的背景总是很重要。
    • 项目搬到github上,这里是DistinctBy的代码:github.com/morelinq/MoreLINQ/blob/master/MoreLinq/DistinctBy.cs
    • 我认为这是对众多GroupBy()/group by/ToLookup() 答案的更好解决方案,因为就像Distinct() 一样,这可以将yield 元素作为一旦遇到(第一次),而那些其他方法在整个输入序列被消耗完之前无法返回任何内容。我认为这是一个重要的,呃,区别值得在答案中指出。此外,就内存而言,通过最后一个元素,此 HashSet&lt;&gt; 将仅存储 unique 元素,而其他方法将在某处存储带有 unique + duplicates 元素的 unique 组。
    【解决方案3】:

    用途:

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

    【讨论】:

    • 完美,无需扩展 Linq 或使用其他依赖项即可工作。
    • 这里有一个很好的答案。谢谢
    【解决方案4】:

    如果你希望它看起来像 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();
    

    【讨论】:

    • 嗯,我的想法是查询语法和流畅的 API 语法都与 LINQ 一样,而且它只是偏爱人们使用的语法。我自己更喜欢流畅的 API,所以我会认为它更像 LINK,但我想这是主观的
    • LINQ-Like 与偏好无关,“LINQ-like”与看起来像是嵌入到 C# 中的不同查询语言有关,我更喜欢流畅的界面,来自 java 流,但它不像 LINQ。
    • 太棒了!!你是我的英雄!
    【解决方案5】:

    我觉得够了:

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

    【讨论】:

    • 如果他需要返回他的全部对象,而不仅仅是那个特定的字段怎么办?
    • 这几个具有相同属性值的对象究竟是什么对象?
    【解决方案6】:

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

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

    【讨论】:

      【解决方案7】:

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

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

      【讨论】:

        【解决方案8】:

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

        【讨论】:

        • 这给了我一个“序列没有值错误”,但 Skeet 的回答产生了正确的结果。
        【解决方案9】:

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

        【讨论】:

        • 您的文章有错误,在 Distinct 之后应该有一个 : public static IEnumerable Distinct(this... 而且它看起来不会(很好)在更多一个属性,即名字和姓氏的组合。
        • 请不要在外部链接an answer must stand on its own中发布相关信息。可以发布链接,但请将相关信息复制到答案本身。您只发布了一个使用示例,但没有外部资源它是无用的。
        【解决方案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 实现......)

        【讨论】:

          【解决方案11】:

          从 .NET 6 开始,有使用the new DistinctBy() extension in Linq 的新解决方案,所以我们可以这样做

          var distinctPersonsById = personList.DistinctBy(x => x.Id);
          

          【讨论】:

            【解决方案12】:

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

            使用 Microsoft.Ajax.Utilities;

            然后像下面这样使用它:

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

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

            【讨论】:

              【解决方案13】:

              你可以这样做(尽管不是闪电般快速):

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

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

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

              【讨论】:

                【解决方案14】:

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

                这是你使用它的方式:

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

                【讨论】:

                  【解决方案15】:

                  当我们在项目中遇到这样的任务时,我们定义了一个小的 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

                  【讨论】:

                    【解决方案16】:

                    如果您不想为了获得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 集合不包含任何空元素

                    如果集合可以包含空值,则只需重写 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() 方法即可过滤掉任何重复项。

                    【讨论】:

                    • 我只想获取字典中的唯一项,请您帮忙,我正在使用此代码 If TempDT IsNot Nothing Then m_ConcurrentScriptDictionary = TempDT.AsEnumerable.ToDictionary(Function(x) x.SafeField(fldClusterId , NULL_ID_VALUE), Function(y) y.SafeField(fldParamValue11, NULL_ID_VALUE))
                    【解决方案17】:

                    覆盖 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() 就足够了,但是如果我们需要性能,我们必须重写它。所有比较算法,首先检查hash,如果相等则调用Equals()。
                    • 另外,在 Equals() 中的第一行应该是“if (!(obj is Person)) return false”。但最佳实践是使用转换为类型的单独对象,例如“var o = obj as Person;if (o==null) return false;”然后用 o 检查相等性而不进行强制转换
                    • 像这样覆盖 Equals 并不是一个好主意,因为它可能会对其他期望 Person 的 Equality 取决于多个属性的程序员产生意想不到的后果。
                    【解决方案18】:

                    与其他 .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),但如果您需要一些在整个代码中都是通用的,本文中的解决方案很棒。

                    【讨论】:

                      【解决方案19】:
                      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() 使用该属性来确定唯一性,因此这不起作用。
                      【解决方案20】:

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

                      【讨论】:

                      • 我不推荐这种方法。虽然它可能在这种特定情况下有效,但这只是一种不好的做法。如果他想通过其他地方的不同属性来区分怎么办?他肯定不能两次覆盖 Equals,对吗? :-) 除此之外,为此目的重写 equals 是根本错误的,因为它是为了判断两个对象是否相等。如果平等的类条件因任何原因发生变化,你肯定会烫到你的手指......
                      【解决方案21】:

                      如果你使用旧的.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);
                      

                      【讨论】:

                      • 这如何改进提供相同扩展方法但实现方式略有不同的公认答案?
                      • 至少更短。而且不是一点点,它的实现方式不同。
                      • 并没有更好。接受的答案要好得多。为什么要提供劣质的解决方案?旧问题的新答案应该是对已有问题的重大改进。
                      【解决方案22】:

                      这可能会有所帮助,试试这个。使用 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)));
                      }
                      

                      【讨论】:

                        【解决方案23】:

                        请尝试以下代码。

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

                        【讨论】:

                        • 欢迎提供简短的回答,但是对于试图了解问题背后原因的后者用户而言,它不会提供太多价值。请抽出一些时间来解释导致问题的真正问题是什么以及如何解决。谢谢~
                        猜你喜欢
                        • 1970-01-01
                        • 1970-01-01
                        • 1970-01-01
                        • 2016-06-15
                        • 1970-01-01
                        • 1970-01-01
                        • 2022-07-11
                        • 1970-01-01
                        • 1970-01-01
                        相关资源
                        最近更新 更多