How to use LINQ to select object with minimum or maximum property value
我有一个具有可以为空的出生日期属性的Person对象。有没有一种方法可以使用LINQ查询具有最早/最小出生日期值的人对象列表?
以下是我的出发点:
1 | var firstBornDate = People.Min(p => p.DateOfBirth.GetValueOrDefault(DateTime.MaxValue)); |
空的dateofbirth值设置为datetime.maxvalue,以便将其排除在最小考虑范围之外(假设至少有一个具有指定的dob)。
但对我来说,所做的就是将firstborndate设置为datetime值。我想得到的是与之匹配的Person对象。是否需要编写第二个这样的查询:
1 | var firstBorn = People.Single(p=> (p.DateOfBirth ?? DateTime.MaxValue) == firstBornDate); |
还是有更精简的方法?
1 2 | People.Aggregate((curMin, x) => (curMin == null || (x.DateOfBirth ?? DateTime.MaxValue) < curMin.DateOfBirth ? x : curMin)) |
不幸的是,没有内置的方法可以做到这一点。
PM> Install-Package morelinq
1 | var firstBorn = People.MinBy(p => p.DateOfBirth ?? DateTime.MaxValue); |
或者,您可以使用我们在morelinq和minby.cs中得到的实现。(当然有一个对应的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | 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; } } |
请注意,如果序列为空,这将引发异常,如果有多个元素,则返回具有最小值的第一个元素。
注意:为了完整性,我将这个答案包括在内,因为OP没有提到数据源是什么,我们不应该做任何假设。
这个查询给出了正确的答案,但可能会变慢,因为它可能需要根据
1 | var oldest = People.OrderBy(p => p.DateOfBirth ?? DateTime.MaxValue).First(); |
更新:实际上我不应该称这个解决方案为"幼稚的",但是用户需要知道他在查询什么。此解决方案的"慢度"取决于基础数据。如果这是一个数组或
1 | People.OrderBy(p => p.DateOfBirth.GetValueOrDefault(DateTime.MaxValue)).First() |
会成功的
所以你要的是
我一直在寻找一种干净有效的方法来做到这一点。我想我找到了一个:
这种模式的一般形式是:
1 2 3 4 | var min = data.Select(x => (key(x), x)).Min().Item2; ^ ^ ^ the sorting key | take the associated original item Min by key(.) |
特别是,使用原始问题中的示例:
对于支持值元组的C 7.0及以上版本:
1 | var youngest = people.Select(p => (p.DateOfBirth, p)).Min().Item2; |
对于7.0之前的C版本,可以使用匿名类型:
1 |
因为值元组和匿名类型都有合理的默认比较器:对于(x1,y1)和(x2,y2),它首先比较
由于匿名类型和值元组都是值类型,所以它们都应该非常有效。
注释
在我上面的
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).
它可以通过预过滤来实现。
1 | people.Where(p => p.DateOfBirth.HasValue) |
因此,实施
注释2
上述方法有一个警告,即当有两个实例具有相同的最小值时,
At least one object must implement IComparable
幸运的是,这仍然可以相当干净地修复。其思想是将一个distanct"id"与每个条目关联起来,这些条目充当明确的连接断路器。我们可以为每个条目使用一个增量ID。仍以年龄为例:
1 2 | var youngest = Enumerable.Range(0, int.MaxValue) .Zip(people, (idx, ppl) => (ppl.DateOfBirth, idx, ppl)).Min().Item3; |
不带额外软件包的解决方案:
1 2 | var min = lst.OrderBy(i => i.StartDate).FirstOrDefault(); var max = lst.OrderBy(i => i.StartDate).LastOrDefault(); |
也可以将其包装成扩展名:
1 2 3 4 5 6 7 8 9 10 11 12 | 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(); } } |
在这种情况下:
1 2 | var min = lst.MinBy(i => i.StartDate); var max = lst.MaxBy(i => i.StartDate); |
顺便说一句。。。O(n^2)不是最佳解决方案。保罗贝茨给出了比我更大的解决方案。但我的仍然是LINQ解决方案,它比这里的其他解决方案更简单和简短。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
非常简单地使用聚合(相当于其他语言的折叠):
1 | var firstBorn = People.Aggregate((min, x) => x.DateOfBirth < min.DateOfBirth ? x : min); |
唯一的缺点是,每个序列元素访问属性两次,这可能很昂贵。这很难解决。
下面是更通用的解决方案。它基本上执行相同的操作(按O(N)顺序),但可以在任何IEnumerable类型上执行,并且可以与属性选择器可以返回空值的类型混合。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | 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; }); } } |
测验:
1 2 | var nullableInts = new int?[] {5, null, 1, 4, 0, 3, null, 1}; Assert.AreEqual(0, nullableInts.MinBy(i => i));//should pass |
再次编辑:
对不起的。除了缺少nullable之外,我在看错误的函数,
Min<(of<(tsource,tresult>)>)(ienumerable<(of<(tsource>)>),func<(of<(tsource,tresult>)>)按您所说的返回结果类型。
我想说一个可能的解决方案是实现IComparable并使用Min<(of<(tsource>)>)(IEnumerable<(of<(tsource>)>),这确实会从IEnumerable返回一个元素。当然,如果您不能修改元素,这对您没有帮助。我觉得微软的设计有点奇怪。
当然,如果需要的话,您可以始终执行for循环,或者使用jon skeet提供的morelinq实现。
我在找类似的东西,最好不用图书馆或排序整个列表。我的解决方案最终类似于问题本身,只是稍微简化了一点。
1 | var firstBorn = People.FirstOrDefault(p => p.DateOfBirth == People.Min(p2 => p2.DateOfBirth)); |
要从对象数组中获取属性的最大值或最小值,请执行以下操作:
列出存储每个属性值的列表:
1 |
将所有属性值添加到列表:
1 2 | foreach (int i in obj.desiredProperty) { values.add(i); } |
从列表中获取最大值或最小值:
1 2 | int Max = values.Max; int Min = values.Min; |
现在,您可以循环遍历对象数组,并将要检查的属性值与max或min int进行比较:
1 2 3 4 5 6 7 8 | foreach (obj o in yourArray) { if (o.desiredProperty == Max) {return o} else if (o.desiredProperty == Min) {return o} } |