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


464

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

这是我开始的:

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

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

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

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

还是有一种更精简的方法?


24
只是对您的示例进行评论:您可能不应该在此处使用Single。如果两个人的出生
Niki,2009年

1
另请参见几乎重复的stackoverflow.com/questions/2736236/…,其中包含一些简洁的示例。
goodeye

4
多么简单实用的功能。MinBy应该在标准库中。我们应该向Microsoft github.com/dotnet/corefx
Panic

2
今天似乎确实存在,只是提供了一个选择属性的功能:a.Min(x => x.foo);
jackmott

4
演示问题:在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是任何单词的最长长度,但不会告诉您最长的单词什么。
上校恐慌

Answers:


297
People.Aggregate((curMin, x) => (curMin == null || (x.DateOfBirth ?? DateTime.MaxValue) <
    curMin.DateOfBirth ? x : curMin))

16
比仅实现IComparable并使用Min(或for循环)慢一点。但是+1为O(n)线性解决方案。
马修·弗拉申

3
另外,它必须是<curmin.DateOfBirth。否则,您正在将DateTime与Person进行比较。
马修·弗拉申

2
在比较两个日期时间时也要小心。我用它来查找无序集合中的最后一个更改记录。它失败了,因为我想要的记录以相同的日期和时间结束。
西蒙·吉尔

8
为什么要进行多余的检查curMin == nullcurMin可能只有null当您使用Aggregate()的种子是null
晚安书呆子骄傲


226

不幸的是,没有内置的方法可以执行此操作,但是它很容易实现。这是它的胆量:

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

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

另外,您可以使用MinLIN.csMoreLINQ中提供的实现。(MaxBy当然有相应的。)

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

PM>安装包morelinq


1
我将用foreach替换Ienumerator +的同时
ggf31416

5
由于在循环之前首次调用MoveNext(),因此无法轻松做到这一点。有其他选择,但它们比IMO更混乱。
乔恩·斯基特

2
虽然我可以返回default(T),但我觉得不合适。这与First()之类的方法和Dictionary indexer的方法更加一致。如果需要,您可以轻松调整它。
乔恩·斯基特

8
由于非图书馆解决方案,我将答案授予了Paul,但是感谢您提供此代码并链接到MoreLINQ库,我想我将开始使用它!
slolife,


135

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

该查询给出了正确的答案,但是可能会变慢,因为它可能必须根据中的数据结构对中的所有项目进行排序:PeoplePeople

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

更新:实际上,我不应该将此解决方案称为“天真”,但用户确实需要知道他要查询的内容。该解决方案的“慢度”取决于基础数据。如果这是一个数组或List<T>,则LINQ to Objects只能选择先对整个集合进行排序,然后再选择第一个项目。在这种情况下,它将比建议的其他解决方案慢。但是,如果这是LINQ to SQL表并且DateOfBirth是索引列,则SQL Server将使用索引而不是对所有行进行排序。其他自定义IEnumerable<T>实现也可以使用索引(请参阅i4o:索引LINQ或对象数据库db4o),并使此解决方案比Aggregate()MaxBy()/ 更快。MinBy(),它需要对整个集合进行一次迭代。实际上,从理论上讲,LINQ to Objects可以OrderBy()对排序后的集合进行特殊处理,例如SortedList<T>,但据我所知没有。


1
有人已经发布了该消息,但在我评论了它的速度(以及占用的空间)有多慢之后(最好是O(n log n)速度与min的O(n)相比,显然已经删除了)。:)
马修·弗拉申

是的,因此,我警告我不要太幼稚:)但是,它非常简单,在某些情况下(小集合或DateOfBirth是索引的DB列)可能可以使用
Lucas,2009年

另一个特殊情况(也不存在)是可以使用orderby的知识,并且无需排序即可首先搜索最低值。
符文FS

对集合进行排序是Nlog(N)操作,它不比线性或O(n)时间复杂度更好。如果我们只需要最小或最大序列中的1个元素/对象,我认为我们应该坚持线性时间复杂度。
Yawar Murtaza

@yawar该集合可能已被排序(更可能被索引),在这种情况下,您可以设置O(log n)
Rune FS

63
People.OrderBy(p => p.DateOfBirth.GetValueOrDefault(DateTime.MaxValue)).First()

会做到的


1
这个很棒!我在linq投影情况下使用OrderByDesending(...)。Take(1)。
VedranMandić2015年

1
这使用排序,超过O(N)时间,也使用O(N)内存。
George Polevoy '16

@GeorgePolevoy假设我们对数据源了解很多。如果数据源已在给定字段上具有排序索引,则该常量将为(低)常量,并且比遍历整个列表所需的可接受答案快很多。如果另一方面,例如,数据源是数组,那么您当然是对的
Rune FS

@RuneFS-仍然应该在答案中提及这一点,因为它很重要。
rory.ap

表演会拖累您。我很难学。如果您希望对象具有“最小值”或“最大值”,则无需对整个数组进行排序。仅扫描一次就足够了。查看接受的答案或查看MoreLinq软件包。
Sau001 '19

35

因此,您要的是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(.)

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

对于支持值元组的 C#7.0及更高版本:

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

对于7.0之前的C#版本,可以改用匿名类型

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

他们的工作,因为这两个值的元组和匿名类型有合理的默认comparers:对(X1,Y1)和(x2,y2)的,它首先比较x1VS x2,然后y1VS y2。这就是为什么内置.Min可以在这些类型上使用函数。

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

注意

在我的上述ArgMin实现中DateOfBirthDateTime为简单起见,我假定采用type 。原始问题要求排除那些具有空DateOfBirth字段的条目:

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

可以通过预过滤来实现

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;

1
当值类型是排序键时,这似乎不起作用。“至少一个对象必须实现IComparable”
liang

1
太好了!这应该是最好的答案。
Guido Mocha '18

@liang是的,很好。幸运的是,仍然有一个干净的解决方案。请参阅“注释2”部分中更新的解决方案。
KFL

选择可以给你ID!var youngest = people.Select((p,i)=>(p.DateOfBirth,i,p))。Min()。Item2;
杰里米

19

无需额外包装的解决方案:

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解决方案,比这里的其他解决方案更简单,更短。


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

3

完全简单地使用汇总(相当于其他语言中的fold):

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

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


1

以下是更通用的解决方案。它基本上执行相同的操作(以O(N)顺序),但是在任何IEnumberable类型上,都可以与属性选择器可以返回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

0

再次编辑:

抱歉。除了缺少可空值之外,我还在看错误的函数,

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

我会说一个可能的解决方案是实现IComparable并使用Min <(Of <(TSource>)>)(IEnumerable <(Of <(TSource>)>)),这实际上确实从IEnumerable返回了一个元素。当然,如果您不能修改元素,那对您没有帮助。我在这里发现MS的设计有点奇怪。

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


0

另一个可以与可为空的选择器键一起使用的实现,如果找不到合适的元素,则对于引用类型的集合将返回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);

-2

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

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

在您的linq语句之前获取最小值会不会更有效?var min = People.Min(...); var firstBorn = People.FirstOrDefault(p => p.DateOfBirth == min...否则,它会反复获取最小值,直到找到您要查找的最小值为止。
Nieminen
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.