如何使用 LINQ 选择具有最小或最大属性值的对象

Posted

技术标签:

【中文标题】如何使用 LINQ 选择具有最小或最大属性值的对象【英文标题】:How to use LINQ to select object with minimum or maximum property value 【发布时间】: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 。如果两个人的出生日期相同,它会抛出一个异常 另见几乎重复的***.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 是任何单词的最长长度,但不会告诉你最长的单词 是什么 【参考方案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 的种子。 第二。 "The first element of source is used as the initial aggregate value."【参考方案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 元素。

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

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

PM> Install-Package morelinq

【讨论】:

我会用 foreach 替换 Ienumerator + while 由于在循环之前第一次调用 MoveNext(),因此无法轻松做到这一点。有替代品,但它们更混乱 IMO。 虽然我可以返回我觉得不合适的默认值(T)。这与 First() 等方法和 Dictionary 索引器的方法更加一致。不过,如果您愿意,您可以轻松调整它。 由于非库解决方案,我将答案授予 Paul,但感谢此代码和 MoreLINQ 库的链接,我想我会开始使用它! @HamishGrubijan: ThrowHelper: code.google.com/p/morelinq/source/browse/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.MaxBy 和 Enumerable.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,那会有用吗? 谢谢。您应该在第一时间指出这一点,而不是给人留下错误的印象。我们是人,所以我们会犯错。最好指出错误并尝试提出解决方案。那会让人们过日子。 :)

以上是关于如何使用 LINQ 选择具有最小或最大属性值的对象的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Asp.net MVC C# 中使用 Linq 从多个表中选择具有最大计数值的记录

LINQ:获取包含具有特定名称和值的属性的数组中的对象

c# 如何获取列表中某个属性最大或最小的元素?

如何选择具有最低属性 linq 查询语法的对象

如何让 LINQ 返回具有给定属性最大值的对象? [复制]

Linq 查询以返回具有特定属性值的嵌套数组