首页 文章

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

提问于
浏览
381

我有一个具有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 回答

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

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

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

    和分钟:

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

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

    var MaxValue = dbContext.YourDataClass.Select(x => x.ColumnToFindMaxValueFrom).Max();
    var MinValue = dbContext.YourDataClass.Select(x => x.ColumnToFindMinValueFrom).Min();
    
  • 59
    People.Aggregate((curMin, x) => (curMin == null || (x.DateOfBirth ?? DateTime.MaxValue) <
        curMin.DateOfBirth ? x : curMin))
    
  • 108

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

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

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

  • 257

    注意:由于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
    People.OrderBy(p => p.DateOfBirth.GetValueOrDefault(DateTime.MaxValue)).First()
    

    会做的伎俩

  • 2

    所以你要求 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;
    
  • 4

    以下是更通用的解决方案 . 它基本上做同样的事情(以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循环,或者使用更多LINQ实施Jon Skeet给出了 .

  • 17

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

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

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

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

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

相关问题