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

slo*_*ife 437 .net c# linq

我有一个具有Nullable DateOfBirth属性的Person对象.有没有办法使用LINQ查询具有最早/最小DateOfBirth值的Person对象列表.

这是我开始的:

var firstBornDate = People.Min(p => p.DateOfBirth.GetValueOrDefault(DateTime.MaxValue));
Run Code Online (Sandbox Code Playgroud)

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

但对我来说,所有这一切都是将firstBornDate设置为DateTime值.我想得到的是与之匹配的Person对象.我是否需要编写第二个查询:

var firstBorn = People.Single(p=> (p.DateOfBirth ?? DateTime.MaxValue) == firstBornDate);
Run Code Online (Sandbox Code Playgroud)

或者有更简洁的方法吗?

Ana*_*tts 282

People.Aggregate((curMin, x) => (curMin == null || (x.DateOfBirth ?? DateTime.MaxValue) <
    curMin.DateOfBirth ? x : curMin))
Run Code Online (Sandbox Code Playgroud)

  • 可能比仅仅实现IComparable并使用Min(或for循环)慢一点.但是对于O(n)linqy解决方案+1. (15认同)
  • 你为什么要做多余的检查`curMin == null`?如果你使用`Aggregate()`和一个'null`的种子,`curMin`只能是'null`. (8认同)
  • 借调.["源的第一个元素用作初始聚合值."](https://msdn.microsoft.com/en-us/library/bb548651(v = vs.110).aspx) (5认同)
  • @Matthew啊是的,修好了 (4认同)
  • 此外,它需要<curmin.DateOfBirth.否则,您将DateTime与Person进行比较. (3认同)
  • 使用它比较两个日期时也要小心.我正在使用它来查找无序集合中的最后一个更改记录.它失败了,因为我想要的记录最终得到了相同的日期和时间. (2认同)

Jon*_*eet 215

不幸的是,没有内置的方法来做到这一点.

PM> Install-Package morelinq

var firstBorn = People.MinBy(p => p.DateOfBirth ?? DateTime.MaxValue);
Run Code Online (Sandbox Code Playgroud)

另外,您也可以使用我们已经有了在实施MoreLINQ,在MinBy.cs.(MaxBy当然有一个相应的.)以下是它的内容:

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

请注意,如果序列为空,这将抛出异常,如果有多个,则返回具有最小值的第一个元素.

  • 由于非库解决方案,我将答案授予Paul,但感谢此代码并链接到MoreLINQ库,我想我将开始使用它! (8认同)
  • 由于在循环之前首次调用MoveNext(),因此无法轻松完成.有其他选择,但他们是更混乱的IMO. (5认同)
  • 虽然我*可以*返回默认(T),这对我来说不合适.这与First()和Dictionary索引器的方法更为一致.如果你愿意,你可以轻松地调整它. (2认同)

Luc*_*cas 123

注意:由于OP没有提到数据源是什么而且我们不应该做出任何假设,因此我将此答案包含在内以便完整.

此查询提供了正确的答案,但可能会更慢,因为它可能必须对所有项目进行排序People,具体取决于数据结构People:

var oldest = People.OrderBy(p => p.DateOfBirth ?? DateTime.MaxValue).First();
Run Code Online (Sandbox Code Playgroud)

更新:实际上我不应该把这个解决方案称为"天真",但用户确实需要知道他正在查询的内容.该解决方案的"缓慢"取决于底层数据.如果这是一个数组或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>,但据我所知,它没有.

  • 有人已经发布了它,但在我评论它有多慢(和占用空间)后,显然删除了它(与 O(n) for min 相比,O(n log n) 速度最好)。:) (3认同)

Run*_* FS 60

People.OrderBy(p => p.DateOfBirth.GetValueOrDefault(DateTime.MaxValue)).First()
Run Code Online (Sandbox Code Playgroud)

会做的伎俩

  • 这个使用排序,超过 O(N) 时间,也使用 O(N) 内存。 (2认同)

KFL*_*KFL 26

所以你要求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(.)
Run Code Online (Sandbox Code Playgroud)

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

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

var youngest = people.Select(p => (p.DateOfBirth, p)).Min().Item2;
Run Code Online (Sandbox Code Playgroud)

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

var youngest = people.Select(p => new { ppl = p; age = p.DateOfBirth }).Min().ppl;
Run Code Online (Sandbox Code Playgroud)

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

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

注意

在上面的ArgMin实现中,我假设DateOfBirth采用类型DateTime来简化和清晰.原始问题要求使用空DateOfBirth字段排除这些条目:

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

它可以通过预过滤实现

people.Where(p => p.DateOfBirth.HasValue)
Run Code Online (Sandbox Code Playgroud)

因此,实施ArgMin或实施的问题并不重要ArgMax.

笔记2

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

至少有一个对象必须实现IComparable

幸运的是,这仍然可以相当干净地修复.这个想法是将一个分散的"ID"与作为明确的打破平局的每个条目联系起来.我们可以为每个条目使用增量ID.仍以人民年龄为例:

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

  • 当值类型是排序键时,这似乎不起作用。“至少一个对象必须实现 IComparable” (2认同)
  • 选择可以给你ID!var youngest = people.Select((p, i) =&gt; (p.DateOfBirth, i, p)).Min().Item2; (2认同)

And*_*rew 18

没有额外包装的解决方案:

var min = lst.OrderBy(i => i.StartDate).FirstOrDefault();
var max = lst.OrderBy(i => i.StartDate).LastOrDefault();
Run Code Online (Sandbox Code Playgroud)

你也可以把它包装成扩展名:

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();
    }
}
Run Code Online (Sandbox Code Playgroud)

在这种情况下:

var min = lst.MinBy(i => i.StartDate);
var max = lst.MaxBy(i => i.StartDate);
Run Code Online (Sandbox Code Playgroud)

顺便说一句...... O(n ^ 2)不是最好的解决方案.罗贝茨给了我最好的解决方案.但我仍然是LINQ解决方案,它比其他解决方案更简单,更简洁.


Vij*_*mal 7

从 .Net 6(预览版 7)或更高版本开始,有新的内置方法Enumerable.MaxByEnumerable.MinBy来实现此目的。

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

var firstBorn = people.MinBy(p => p.DateOfBirth);
Run Code Online (Sandbox Code Playgroud)


hon*_*gxu 5

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

People.MinBy(p => p.DateOfBirth)