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

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

这是我开始的:

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

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

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

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

或者有更精简的方法吗?

回答(12)

2 years ago

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

2 years ago

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

PM> Install-Package morelinq

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

或者,您可以使用我们在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;
    }
}

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

2 years ago

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

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

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

更新:实际上我不应该't call this solution 755808 , but the user does need to know what he is querying against. This solution' s "slowness"取决于基础数据 . 如果这是一个数组或 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> 这样的已排序集合创建特殊情况,但据我所知,它并没有 .

2 years ago

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

会做的伎俩

2 years ago

所以你要求 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(.)

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

For C# 7.0 and above that supports value tuple:

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

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

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

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

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

NOTE

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

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

它可以通过预过滤实现

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

因此,实施 ArgMinArgMax 的问题并不重要 .

NOTE 2

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

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

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

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

2 years ago

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

2 years ago

没有检查,但这必须做预期的事情:

var itemWithMaxValue = SomeListOfClass.OrderByDescending(i => i.SomeFloat).FirstOrDefault();

和分钟:

var itemWithMinValue = SomeListOfClass.OrderByDescending(i => i.SomeFloat).LastOrDefault();

2 years ago

只检查过Entity framework 6.0.0>:这可以完成:

var MaxValue = dbContext.YourDataClass.Select(x => x.ColumnToFindMaxValueFrom).Max();
var MinValue = dbContext.YourDataClass.Select(x => x.ColumnToFindMinValueFrom).Min();

2 years ago

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

2 years ago

再次编辑:

抱歉 . 除了错过可空之外,我正在寻找错误的功能,

Min<(Of <(TSource, TResult>)>)(IEnumerable<(Of <(TSource>)>), Func<(Of <(TSource, TResult>)>))会按照您的说法返回结果类型 .

我想说一个可能的解决方案是实现IComparable并使用Min<(Of <(TSource>)>)(IEnumerable<(Of <(TSource>)>)),它确实从IEnumerable返回一个元素 . 当然,这不会修改元素 . 我发现MS的设计在这里有点奇怪 .

当然,如果需要,你总是可以进行for循环,或者使用更多LINQ实施Jon Skeet给出了 .

2 years ago

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

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

2 years ago

要从对象数组中获取属性的最大值或最小值:

创建一个存储每个属性值的列表:

list<int> values = new list<int>;

将所有属性值添加到列表:

foreach (int i in obj.desiredProperty)
{    values.add(i);  }

从列表中获取最大值或最小值:

int Max = values.Max;
int Min = values.Min;

现在,您可以循环遍历对象数组,并将要检查的属性值与max或min int进行比较:

foreach (obj o in yourArray)
{
    if (o.desiredProperty == Max)
       {return o}

    else if (o.desiredProperty == Min)
        {return o}
}