【问题标题】:How to use LINQ to select object with minimum or maximum property value如何使用 LINQ 选择具有最小或最大属性值的对象
【发布时间】:2010-10-29 04:26:04
【问题描述】:

我有一个带有 Nullable DateOfBirth 属性的 Person 对象。有没有一种方法可以使用 LINQ 查询 Person 对象列表中具有最早/最小 DateOfBirth 值的对象?

这是我开始的:

var firstBornDate = People.Min(p => p.DateOfBirth.GetValueOrDefault(DateTime.MaxValue));

将 Null DateOfBirth 值设置为 DateTime.MaxValue 以便将它们排除在 Min 考虑之外(假设至少有一个具有指定的出生日期)。

但我所做的只是将 firstBornDate 设置为 DateTime 值。我想得到的是与之匹配的 Person 对象。我是否需要像这样编写第二个查询:

var firstBorn = People.Single(p=> (p.DateOfBirth ?? DateTime.MaxValue) == firstBornDate);

或者有更精简的方法吗?

【问题讨论】:

  • 只是对您的示例的评论:您可能不应该在这里使用 Single 。如果两个人的出生日期相同,它会抛出一个异常
  • 另见几乎重复的stackoverflow.com/questions/2736236/…,其中有一些简洁的例子。
  • 多么简单实用的功能。 MinBy 应该在标准库中。我们应该向 Microsoft github.com/dotnet/corefx 提交拉取请求
  • 今天似乎确实存在,只需提供一个函数来选择属性:a.Min(x => x.foo);
  • 为了演示问题:在 Python 中,max("find a word of maximal length in this sentence".split(), key=len) 返回字符串 'sentence'。在 C# 中,"find a word of maximal length in this sentence".Split().Max(word => word.Length) 计算出 8 是任何单词的最长长度,但不会告诉你最长的单词 是什么

标签: c# .net linq


【解决方案1】:
People.Aggregate((curMin, x) => (curMin == null || (x.DateOfBirth ?? DateTime.MaxValue) <
    curMin.DateOfBirth ? x : curMin))

【讨论】:

  • 可能比仅实现 IComparable 和使用 Min(或 for 循环)慢一点。但是对于 O(n) linqy 解决方案 +1。
  • 另外,它需要是
  • 在使用它比较两个日期时间时也要小心。我正在使用它来查找无序集合中的最后一个更改记录。它失败了,因为我想要的记录以相同的日期和时间结束。
  • 你为什么要做多余的检查curMin == nullcurMin 只能是 null 如果您使用 Aggregate()null 的种子。
【解决方案2】:

不幸的是,没有内置的方法可以做到这一点,但你自己实现它很容易。这是它的胆量:

public static TSource MinBy<TSource, TKey>(this IEnumerable<TSource> source,
    Func<TSource, TKey> selector)
{
    return source.MinBy(selector, null);
}

public static TSource MinBy<TSource, TKey>(this IEnumerable<TSource> source,
    Func<TSource, TKey> selector, IComparer<TKey> comparer)
{
    if (source == null) throw new ArgumentNullException("source");
    if (selector == null) throw new ArgumentNullException("selector");
    comparer ??= Comparer<TKey>.Default;

    using (var sourceIterator = source.GetEnumerator())
    {
        if (!sourceIterator.MoveNext())
        {
            throw new InvalidOperationException("Sequence contains no elements");
        }
        var min = sourceIterator.Current;
        var minKey = selector(min);
        while (sourceIterator.MoveNext())
        {
            var candidate = sourceIterator.Current;
            var candidateProjected = selector(candidate);
            if (comparer.Compare(candidateProjected, minKey) < 0)
            {
                min = candidate;
                minKey = candidateProjected;
            }
        }
        return min;
    }
}

示例用法:

var firstBorn = People.MinBy(p => p.DateOfBirth ?? DateTime.MaxValue);

请注意,如果序列为空,这将引发异常,如果有多个,则返回具有最小值的 first 元素。

或者,您可以使用我们在MoreLINQMinBy.cs 中的实现。 (当然有对应的MaxBy。)

通过包管理器控制台安装:

PM> Install-Package morelinq

【讨论】:

  • 我会用 foreach 替换 Ienumerator + while
  • 由于在循环之前第一次调用 MoveNext(),因此无法轻松做到这一点。有替代品,但它们更混乱 IMO。
  • 虽然我可以返回我觉得不合适的默认值(T)。这与 First() 等方法和 Dictionary 索引器的方法更加一致。不过,如果您愿意,您可以轻松调整它。
  • 由于非库解决方案,我将答案授予 Paul,但感谢此代码和 MoreLINQ 库的链接,我想我会开始使用它!
【解决方案3】:

注意:为了完整起见,我包含这个答案,因为 OP 没有提到数据源是什么,我们不应该做出任何假设。

这个查询给出了正确的答案,但可能会更慢,因为它可能需要对People 中的所有项进行排序,具体取决于People 的数据结构是:

var oldest = People.OrderBy(p => p.DateOfBirth ?? DateTime.MaxValue).First();

更新:实际上我不应该将此解决方案称为“幼稚”,但用户确实需要知道他在查询什么。该解决方案的“缓慢性”取决于基础数据。如果这是一个数组或List&lt;T&gt;,那么 LINQ to Objects 就别无选择,只能在选择第一项之前先对整个集合进行排序。在这种情况下,它会比建议的其他解决方案慢。但是,如果这是一个 LINQ to SQL 表并且DateOfBirth 是一个索引列,那么 SQL Server 将使用索引而不是对所有行进行排序。其他自定义IEnumerable&lt;T&gt; 实现也可以使用索引(参见i4o: Indexed LINQ,或对象数据库db4o)并使此解决方案比需要迭代整个集合的Aggregate()MaxBy()/MinBy() 更快一次。事实上,LINQ to Objects 可以(理论上)在 OrderBy() 中为像 SortedList&lt;T&gt; 这样的排序集合创建特殊情况,但据我所知,它没有。

【讨论】:

  • 有人已经发布了,但在我评论它有多慢(和占用空间)之后显然删除了它(与 min 的 O(n) 相比,O(n log n) 的速度充其量是 O(n log n) )。 :)
  • 是的,因此我警告说这是一个幼稚的解决方案 :) 但是它非常简单,并且在某些情况下可能可用(小型集合或 DateOfBirth 是索引数据库列)
  • 另一个特殊情况(也不存在)是可以使用 orderby 的知识并首先搜索最小值而不进行排序。
  • 对集合进行排序是 Nlog(N) 操作,它并不优于线性或 O(n) 时间复杂度。如果我们只需要一个最小或最大序列中的 1 个元素/对象,我认为我们应该坚持线性时间复杂性。
  • @yawar 集合可能已经被排序(更有可能被索引),在这种情况下你可以有 O(log n)
【解决方案4】:
People.OrderBy(p => p.DateOfBirth.GetValueOrDefault(DateTime.MaxValue)).First()

会成功的

【讨论】:

  • 这个太棒了!在 linq 投影的情况下,我使用了 OrderByDesending(...).Take(1)。
  • 这个使用排序,超过了O(N)时间,也使用了O(N)内存。
  • @GeorgePolevoy 假设我们对数据源了解很多。如果数据源在给定字段上已经有一个排序索引,那么这将是一个(低)常量,并且比遍历整个列表所需的公认答案要快得多。另一方面,如果数据源是例如你当然是对的数组
  • @RuneFS -- 您仍然应该在回答中提及这一点,因为它很重要。
  • 性能会拖累你。我很难学会。如果您想要具有 Min 或 Max 值的对象,那么您不需要对整个数组进行排序。只需 1 次扫描就足够了。查看接受的答案或查看 MoreLinq 包。
【解决方案5】:

所以你要求ArgMinArgMax。 C# 没有针对这些的内置 API。

我一直在寻找一种干净高效(O(n) 及时)的方法来做到这一点。我想我找到了一个:

这种模式的一般形式是:

var min = data.Select(x => (key(x), x)).Min().Item2;
                            ^           ^       ^
              the sorting key           |       take the associated original item
                                Min by key(.)

特别是使用原始问题中的示例:

对于支持 value tuple 的 C# 7.0 及更高版本:

var youngest = people.Select(p => (p.DateOfBirth, p)).Min().Item2;

对于 7.0 之前的 C# 版本,可以使用 anonymous type 代替:

var youngest = people.Select(p => new {age = p.DateOfBirth, ppl = p}).Min().ppl;

它们之所以有效是因为值元组和匿名类型都有合理的默认比较器:对于 (x1, y1) 和 (x2, y2),它首先比较 x1x2,然后是 y1y2。这就是为什么内置的.Min 可以用于这些类型的原因。

而且由于匿名类型和值元组都是值类型,它们应该都非常高效。

注意

在我上面的ArgMin 实现中,为了简单明了,我假设DateOfBirth 采用DateTime 类型。最初的问题要求排除那些带有 null DateOfBirth 字段的条目:

将 Null DateOfBirth 值设置为 DateTime.MaxValue 以便将它们排除在 Min 考虑之外(假设至少有一个具有指定的出生日期)。

可以通过预过滤来实现

people.Where(p => p.DateOfBirth.HasValue)

所以实现ArgMinArgMax的问题无关紧要。

注意 2

上述方法有一个警告,当有两个实例具有相同的最小值时,Min() 实现将尝试将实例作为决胜局进行比较。但是,如果实例的类没有实现IComparable,则会抛出运行时错误:

至少一个对象必须实现 IComparable

幸运的是,这仍然可以相当干净地修复。这个想法是将一个遥远的“ID”与作为明确的决胜局的每个条目相关联。我们可以为每个条目使用增量 ID。还是以人的年龄为例:

var youngest = Enumerable.Range(0, int.MaxValue)
               .Zip(people, (idx, ppl) => (ppl.DateOfBirth, idx, ppl)).Min().Item3;

【讨论】:

  • 当值类型是排序键时,这似乎不起作用。 "至少一个对象必须实现 IComparable"
  • 太棒了!这应该是最好的答案。
  • @liang 很好。幸运的是,仍然有一个干净的解决方案。请参阅“注 2”部分中的更新解决方案。
  • Select可以给你ID! var youngest = people.Select((p, i) => (p.DateOfBirth, i, p)).Min().Item2;
  • 最后一个解决方案太丑了。 Linq 经常让困难变得简单,让简单变得困难。您的普通程序员真的必须努力工作才能理解该语句在做什么。然后我再次提示你不是一个普通的程序员。
【解决方案6】:

没有额外包的解决方案:

var min = lst.OrderBy(i => i.StartDate).FirstOrDefault();
var max = lst.OrderBy(i => i.StartDate).LastOrDefault();

您也可以将其包装到扩展中:

public static class LinqExtensions
{
    public static T MinBy<T, TProp>(this IEnumerable<T> source, Func<T, TProp> propSelector)
    {
        return source.OrderBy(propSelector).FirstOrDefault();
    }

    public static T MaxBy<T, TProp>(this IEnumerable<T> source, Func<T, TProp> propSelector)
    {
        return source.OrderBy(propSelector).LastOrDefault();
    }
}

在这种情况下:

var min = lst.MinBy(i => i.StartDate);
var max = lst.MaxBy(i => i.StartDate);

顺便说一句... O(n^2) 不是最好的解决方案。 Paul Betts 给出了比我更胖的解决方案。但我的仍然是 LINQ 解决方案,它比这里的其他解决方案更简单、更短。

【讨论】:

    【解决方案7】:

    .NET 6 原生支持 MaxBy/MinBy。所以你可以用一个简单的方法来做到这一点

    People.MinBy(p =&gt; p.DateOfBirth)

    【讨论】:

      【解决方案8】:
      public class Foo {
          public int bar;
          public int stuff;
      };
      
      void Main()
      {
          List<Foo> fooList = new List<Foo>(){
          new Foo(){bar=1,stuff=2},
          new Foo(){bar=3,stuff=4},
          new Foo(){bar=2,stuff=3}};
      
          Foo result = fooList.Aggregate((u,v) => u.bar < v.bar ? u: v);
          result.Dump();
      }
      

      【讨论】:

        【解决方案9】:

        聚合的完美简单使用(相当于其他语言的折叠):

        var firstBorn = People.Aggregate((min, x) => x.DateOfBirth < min.DateOfBirth ? x : min);
        

        唯一的缺点是每个序列元素访问该属性两次,这可能很昂贵。这很难解决。

        【讨论】:

          【解决方案10】:

          从 .Net 6(Preview 7)或更高版本开始,有新的内置方法 Enumerable.MaxByEnumerable.MinBy 来实现这一点。

          var lastBorn = people.MaxBy(p => p.DateOfBirth);
          
          var firstBorn = people.MinBy(p => p.DateOfBirth);
          

          【讨论】:

            【解决方案11】:

            以下是更通用的解决方案。它本质上做同样的事情(以 O(N) 的顺序),但在任何 IEnumerable 类型上,并且可以与属性选择器可以返回 null 的类型混合。

            public static class LinqExtensions
            {
                public static T MinBy<T>(this IEnumerable<T> source, Func<T, IComparable> selector)
                {
                    if (source == null)
                    {
                        throw new ArgumentNullException(nameof(source));
                    }
                    if (selector == null)
                    {
                        throw new ArgumentNullException(nameof(selector));
                    }
            
                    return source.Aggregate((min, cur) =>
                    {
                        if (min == null)
                        {
                            return cur;
                        }
            
                        var minComparer = selector(min);
            
                        if (minComparer == null)
                        {
                            return cur;
                        }
            
                        var curComparer = selector(cur);
            
                        if (curComparer == null)
                        {
                            return min;
                        }
            
                        return minComparer.CompareTo(curComparer) > 0 ? cur : min;
                    });
                }
            }
            

            测试:

            var nullableInts = new int?[] {5, null, 1, 4, 0, 3, null, 1};
            Assert.AreEqual(0, nullableInts.MinBy(i => i));//should pass
            

            【讨论】:

              【解决方案12】:

              试试下面的思路:

              var firstBornDate = People.GroupBy(p => p.DateOfBirth).Min(g => g.Key).FirstOrDefault();
              

              【讨论】:

                【解决方案13】:

                您可以像 SQL 中的 order by 和 limit/fetch 一样进行操作。所以你按 DateOfBirth 升序排序,然后只取第一行。

                var query = from person in People
                            where person.DateOfBirth!=null
                            orderby person.DateOfBirth
                            select person;
                var firstBorn = query.Take(1).toList();
                

                【讨论】:

                • 与多个答案中提出的OrderBy + FirstOrDefault 相同,因此该答案并没有真正添加任何新内容。此外,只有 'Skip` + Take 翻译为限制/获取。 Take(1) 翻译为 TOP(1)。这是关于 LINQ 到对象,而不是 LINQ 到 SQL 后端。
                【解决方案14】:

                再次编辑:

                对不起。除了缺少可空值之外,我还查看了错误的函数,

                Min<(Of <(TSource, TResult>)>)(IEnumerable<(Of <(TSource>)>), Func<(Of <(TSource, TResult>)>)) 确实如您所说返回结果类型。

                我想说一种可能的解决方案是实现 IComparable 并使用 Min<(Of <(TSource>)>)(IEnumerable<(Of <(TSource>)>)),它确实会从 IEnumerable 返回一个元素。当然,如果您无法修改元素,那将无济于事。我觉得这里 MS 的设计有点奇怪。

                当然,如果需要,您始终可以执行 for 循环,或者使用 Jon Skeet 提供的 MoreLINQ 实现。

                【讨论】:

                  【解决方案15】:

                  另一个实现,它可以使用可为空的选择器键,并且对于引用类型的集合,如果没有找到合适的元素,则返回 null。 例如,这可能对处理数据库结果很有帮助。

                    public static class IEnumerableExtensions
                    {
                      /// <summary>
                      /// Returns the element with the maximum value of a selector function.
                      /// </summary>
                      /// <typeparam name="TSource">The type of the elements of source.</typeparam>
                      /// <typeparam name="TKey">The type of the key returned by keySelector.</typeparam>
                      /// <param name="source">An IEnumerable collection values to determine the element with the maximum value of.</param>
                      /// <param name="keySelector">A function to extract the key for each element.</param>
                      /// <exception cref="System.ArgumentNullException">source or keySelector is null.</exception>
                      /// <exception cref="System.InvalidOperationException">source contains no elements.</exception>
                      /// <returns>The element in source with the maximum value of a selector function.</returns>
                      public static TSource MaxBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector) => MaxOrMinBy(source, keySelector, 1);
                  
                      /// <summary>
                      /// Returns the element with the minimum value of a selector function.
                      /// </summary>
                      /// <typeparam name="TSource">The type of the elements of source.</typeparam>
                      /// <typeparam name="TKey">The type of the key returned by keySelector.</typeparam>
                      /// <param name="source">An IEnumerable collection values to determine the element with the minimum value of.</param>
                      /// <param name="keySelector">A function to extract the key for each element.</param>
                      /// <exception cref="System.ArgumentNullException">source or keySelector is null.</exception>
                      /// <exception cref="System.InvalidOperationException">source contains no elements.</exception>
                      /// <returns>The element in source with the minimum value of a selector function.</returns>
                      public static TSource MinBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector) => MaxOrMinBy(source, keySelector, -1);
                  
                  
                      private static TSource MaxOrMinBy<TSource, TKey>
                        (IEnumerable<TSource> source, Func<TSource, TKey> keySelector, int sign)
                      {
                        if (source == null) throw new ArgumentNullException(nameof(source));
                        if (keySelector == null) throw new ArgumentNullException(nameof(keySelector));
                        Comparer<TKey> comparer = Comparer<TKey>.Default;
                        TKey value = default(TKey);
                        TSource result = default(TSource);
                  
                        bool hasValue = false;
                  
                        foreach (TSource element in source)
                        {
                          TKey x = keySelector(element);
                          if (x != null)
                          {
                            if (!hasValue)
                            {
                              value = x;
                              result = element;
                              hasValue = true;
                            }
                            else if (sign * comparer.Compare(x, value) > 0)
                            {
                              value = x;
                              result = element;
                            }
                          }
                        }
                  
                        if ((result != null) && !hasValue)
                          throw new InvalidOperationException("The source sequence is empty");
                  
                        return result;
                      }
                    }
                  
                  

                  例子:

                  public class A
                  {
                    public int? a;
                    public A(int? a) { this.a = a; }
                  }
                  
                  var b = a.MinBy(x => x.a);
                  var c = a.MaxBy(x => x.a);
                  

                  【讨论】:

                    【解决方案16】:

                    如果您想选择具有最小或最大属性值的对象。另一种方法是使用Implementing IComparable。

                    public struct Money : IComparable<Money>
                    {
                       public Money(decimal value) : this() { Value = value; }
                       public decimal Value { get; private set; }
                       public int CompareTo(Money other) { return Value.CompareTo(other.Value); }
                    }
                    

                    最大实现将是。

                    var amounts = new List<Money> { new Money(20), new Money(10) };
                    Money maxAmount = amounts.Max();
                    

                    最低实施将是。

                    var amounts = new List<Money> { new Money(20), new Money(10) };
                    Money maxAmount = amounts.Min();
                    

                    这样就可以比较任意一个对象,在返回对象类型的同时得到最大值和最小值。

                    希望这会对某人有所帮助。

                    【讨论】:

                      【解决方案17】:

                      一种通过 IEnumerable 上的扩展函数返回对象和找到的最小值的方法。它需要一个可以对集合中的对象执行任何操作的 Func:

                      public static (double min, T obj) tMin<T>(this IEnumerable<T> ienum, 
                                  Func<T, double> aFunc)
                              {
                                  var okNull = default(T);
                                  if (okNull != null)
                                      throw new ApplicationException("object passed to Min not nullable");
                      
                                  (double aMin, T okObj) best = (double.MaxValue, okNull);
                                  foreach (T obj in ienum)
                                  {
                                      double q = aFunc(obj);
                                      if (q < best.aMin)
                                          best = (q, obj);
                                  }
                                  return (best);
                              }
                      

                      对象是机场的示例,我们希望找到离给定(纬度、经度)最近的机场。机场有一个 dist(lat, lon) 函数。

                      (double okDist, Airport best) greatestPort = airPorts.tMin(x => x.dist(okLat, okLon));
                      

                      【讨论】:

                        【解决方案18】:

                        我自己也在寻找类似的东西,最好不使用库或对整个列表进行排序。我的解决方案最终类似于问题本身,只是简化了一点。

                        var min = People.Min(p => p.DateOfBirth);
                        var firstBorn = People.FirstOrDefault(p => p.DateOfBirth == min);
                        

                        【讨论】:

                        • 在您的 linq 语句之前获取最小值不是更有效率吗? var min = People.Min(...); var firstBorn = People.FirstOrDefault(p =&gt; p.DateOfBirth == min... 否则它会反复获取最小值,直到找到您要查找的那个。
                        • 这个解决方案分配的可能少于大多数解决方案(没有 GroupBy,但确实创建了 lambdas)并且是 O(n)。而且它比投票最多的聚合解决方案更容易理解。应该投票更高!
                        【解决方案19】:

                        您可以使用现有的 linq 扩展,例如 MoreLinq。但是如果你只需要这些方法,那么你可以使用这里的简单代码:

                        public static IEnumerable<T> MinBys<T>(this IEnumerable<T> collection, Func<T, IComparable> selector)
                        {
                            var dict = collection.GroupBy(selector).ToDictionary(g => g.Key);
                            return dict[dict.Keys.Min()];
                        }
                        public static IEnumerable<T> MaxBys<T>(this IEnumerable<T> collection, Func<T, IComparable> selector)
                        {
                            var dict = collection.GroupBy(selector).ToDictionary(g => g.Key);
                            return dict[dict.Keys.Max()];
                        }
                        

                        【讨论】:

                        • 没用。仅当 selector 产生可比较的类型时,才能使用 Min 和 Max。
                        • 能否请您提供一些没有用处的代码示例?
                        • 只从selector返回一个匿名类型。
                        • 谢谢。那么如果我使用where TVal: IComparable,那会有用吗?
                        • 谢谢。您应该在第一时间指出这一点,而不是给人留下错误的印象。我们是人,所以我们会犯错。最好指出错误并尝试提出解决方案。那会让人们过日子。 :)
                        猜你喜欢
                        • 2021-03-29
                        • 2017-10-03
                        • 2019-10-06
                        • 2014-12-01
                        • 1970-01-01
                        • 1970-01-01
                        • 1970-01-01
                        • 2023-03-08
                        • 2017-09-20
                        相关资源
                        最近更新 更多