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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/914109/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-06 03:04:18  来源:igfitidea点击:

How to use LINQ to select object with minimum or maximum property value

c#.netlinq

提问by slolife

I have a Person object with a Nullable DateOfBirth property. Is there a way to use LINQ to query a list of Person objects for the one with the earliest/smallest DateOfBirth value.

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

Here's what I started with:

这是我的开始:

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

Null DateOfBirth values are set to DateTime.MaxValue in order to rule them out of the Min consideration (assuming at least one has a specified DOB).

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

But all that does for me is to set firstBornDate to a DateTime value. What I'd like to get is the Person object that matches that. Do I need to write a second query like so:

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

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

Or is there a leaner way of doing it?

或者有更精简的方法吗?

采纳答案by Ana Betts

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

回答by Matthew Flaschen

EDIT again:

再次编辑:

Sorry. Besides missing the nullable I was looking at the wrong function,

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

Min<(Of <(TSource, TResult>)>)(IEnumerable<(Of <(TSource>)>), Func<(Of <(TSource, TResult>)>))does return the result type as you said.

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

I would say one possible solution is to implement IComparable and use Min<(Of <(TSource>)>)(IEnumerable<(Of <(TSource>)>)), which really does return an element from the IEnumerable. Of course, that doesn't help you if you can't modify the element. I find MS's design a bit weird here.

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

Of course, you can always do a for loop if you need to, or use the MoreLINQ implementation Jon Skeet gave.

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

回答by Jon Skeet

Unfortunately there isn't a built-in method to do this, but it's easy enough to implement for yourself. Here are the guts of it:

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

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;
    }
}

Example usage:

用法示例:

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

Note that this will throw an exception if the sequence is empty, and will return the firstelement with the minimal value if there's more than one.

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

Alternatively, you can use the implementation we've got in MoreLINQ, in MinBy.cs. (There's a corresponding MaxBy, of course.)

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

Install via package manager console:

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

PM> Install-Package morelinq

PM> 安装包 morelinq

回答by Lucas

NOTE: I include this answer for completeness since the OP didn't mention what the data source is and we shouldn't make any assumptions.

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

This query gives the correct answer, but could be slowersince it might have to sort allthe items in People, depending on what data structure Peopleis:

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

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

UPDATE: Actually I shouldn't call this solution "naive", but the user does need to know what he is querying against. This solution's "slowness" depends on the underlying data. If this is a array or List<T>, then LINQ to Objects has no choice but to sort the entire collection first before selecting the first item. In this case it will be slower than the other solution suggested. However, if this is a LINQ to SQL table and DateOfBirthis an indexed column, then SQL Server will use the index instead of sorting all the rows. Other custom IEnumerable<T>implementations could also make use of indexes (see i4o: Indexed LINQ, or the object database db4o) and make this solution faster than Aggregate()or MaxBy()/MinBy()which need to iterate the whole collection once. In fact, LINQ to Objects could have (in theory) made special cases in OrderBy()for sorted collections like SortedList<T>, but it doesn't, as far as I know.

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

回答by Rune FS

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

Would do the trick

会做的伎俩

回答by JustDave

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

回答by Andrew

Solution with no extra packages:

没有额外包的解决方案:

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

also you can wrap it into extension:

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

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

and in this case:

在这种情况下:

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


By the way... O(n^2) is not the best solution. Paul Bettsgave fatster solution than my. But my is still LINQ solution and it's more simple and more short than other solutions here.

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

回答by Are

I was looking for something similar myself, preferably without using a library or sorting the entire list. My solution ended up similar to the question itself, just simplified a bit.

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

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

回答by zafar

The following is the more generic solution. It essentially does the same thing (in O(N) order) but on any IEnumberable types and can mixed with types whose property selectors could return null.

以下是更通用的解决方案。它本质上做同样的事情(以 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;
        });
    }
}

Tests:

测试:

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

回答by KFL

So you are asking for ArgMinor ArgMax. C# doesn't have a built-in API for those.

所以你要求ArgMinor ArgMax。C# 没有这些的内置 API。

I've been looking for a clean and efficient (O(n) in time) way to do this. And I think I found one:

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

The general form of this pattern is:

这种模式的一般形式是:

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

Specially, using the example in original question:

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

For C# 7.0 and above that supports value tuple:

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

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

For C# version before 7.0, anonymous typecan be used instead:

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

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

They work because both value tuple and anonymous type have sensible default comparers: for (x1, y1) and (x2, y2), it first compares x1vs x2, then y1vs y2. That's why the built-in .Mincan be used on those types.

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

And since both anonymous type and value tuple are value types, they should be both very efficient.

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

NOTE

笔记

In my above ArgMinimplementations I assumed DateOfBirthto take type DateTimefor simplicity and clarity. The original question asks to exclude those entries with null DateOfBirthfield:

在我上面的ArgMin实现中DateOfBirthDateTime为了简单和清晰,我假设采用类型。原始问题要求排除那些带有空DateOfBirth字段的条目:

Null DateOfBirth values are set to DateTime.MaxValue in order to rule them out of the Min consideration (assuming at least one has a specified DOB).

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

It can be achieved with a pre-filtering

它可以通过预过滤来实现

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

So it's immaterial to the question of implementing ArgMinor ArgMax.

所以这对于实现ArgMinor的问题无关紧要ArgMax

NOTE 2

笔记2

The above approach has a caveat that when there are two instances that have the same min value, then the Min()implementation will try to compare the instances as a tie-breaker. However, if the class of the instances does not implement IComparable, then a runtime error will be thrown:

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

At least one object must implement IComparable

至少一个对象必须实现 IComparable

Luckily, this can still be fixed rather cleanly. The idea is to associate a distanct "ID" with each entry that serves as the unambiguous tie-breaker. We can use an incremental ID for each entry. Still using the people age as example:

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

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