C# 如何使用LINQ选择具有最小或最大特性值的对象
我有一个Person对象,其DateOfBirth属性为空。是否有一种方法可以使用LINQ查询具有最早/最小DateOfBirth值的Person对象列表 我从这里开始:C# 如何使用LINQ选择具有最小或最大特性值的对象,c#,.net,linq,C#,.net,Linq,我有一个Person对象,其DateOfBirth属性为空。是否有一种方法可以使用LINQ查询具有最早/最小DateOfBirth值的Person对象列表 我从这里开始: var firstBornDate=People.Min(p=>p.DateOfBirth.GetValueOrDefault(DateTime.MaxValue)); Null DateOfBirth值被设置为DateTime.MaxValue,以便排除最小考虑因素(假设至少有一个具有指定的DOB) 但我所做的就是将fir
var firstBornDate=People.Min(p=>p.DateOfBirth.GetValueOrDefault(DateTime.MaxValue));
Null DateOfBirth值被设置为DateTime.MaxValue,以便排除最小考虑因素(假设至少有一个具有指定的DOB)
但我所做的就是将firstBornDate设置为DateTime值。我想得到的是与之匹配的Person对象。我是否需要像这样编写第二个查询:
var firstBorn=People.Single(p=>(p.DateOfBirth??DateTime.MaxValue)=firstBornDate);
或者有更精简的方法吗?再次编辑:
对不起。除了缺少空值之外,我看到的函数是错误的
按您所说的返回结果类型
我想说一个可能的解决方案是实现IComparable和use,它确实从IEnumerable返回一个元素。当然,如果您不能修改元素,这对您没有帮助。我觉得MS的设计有点奇怪
当然,如果需要,您可以始终执行for循环,或者使用Jon Skeet提供的MoreLINQ实现。People.Aggregate((curMin,x)=>(curMin==null | |(x.DateOfBirth??DateTime.MaxValue)<
People.Aggregate((curMin, x) => (curMin == null || (x.DateOfBirth ?? DateTime.MaxValue) <
curMin.DateOfBirth ? x : curMin))
curMin.DateOfBirth?x:curMin)
不幸的是,没有内置的方法来实现这一点,但它很容易为您自己实现。以下是它的精髓:
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<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;
}
}
请注意,如果序列为空,则会引发异常,如果序列中有多个元素,则会返回具有最小值的第一个元素
或者,您可以使用我们在中的实现。(当然,有一个对应的MaxBy
)
通过软件包管理器控制台安装:
PM>安装软件包morelinq
注:为了完整性,我加入了这个答案,因为OP没有提到数据源是什么,我们不应该做任何假设 此查询给出了正确的答案,但速度可能较慢,因为它可能必须根据
人员
的数据结构对人员
中的所有项目进行排序:
var oldest = People.OrderBy(p => p.DateOfBirth ?? DateTime.MaxValue).First();
更新:实际上我不应该称这个解决方案为“幼稚”,但是用户需要知道他在查询什么。此解决方案的“慢度”取决于底层数据。如果这是一个数组或列表
,则LINQ to Objects别无选择,只能在选择第一项之前先对整个集合进行排序。在这种情况下,它将比建议的其他解决方案慢。但是,如果这是一个LINQ to SQL表,并且DateOfBirth
是一个索引列,那么SQL Server将使用该索引而不是对所有行进行排序。其他自定义的IEnumerable
实现也可以使用索引(请参阅或对象数据库),使此解决方案比需要迭代整个集合一次的Aggregate()
或MaxBy()
/MinBy()
更快。事实上,LINQ to Objects(理论上)可以在OrderBy()
中为排序后的集合(如SortedList
)生成特殊情况,但据我所知,它没有
People.OrderBy(p => p.DateOfBirth.GetValueOrDefault(DateTime.MaxValue)).First()
会做这个把戏的public class Foo{
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();
}
公共酒吧;
公共物品;
};
void Main()
{
列表傻瓜=新列表(){
new Foo(){bar=1,stuff=2},
new Foo(){bar=3,stuff=4},
新的Foo(){bar=2,stuff=3};
Foo结果=傻瓜式聚合((u,v)=>u.bar
无额外软件包的解决方案:
var min = lst.OrderBy(i => i.StartDate).FirstOrDefault();
var max = lst.OrderBy(i => i.StartDate).LastOrDefault();
您还可以将其包装为扩展:
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();
}
}
顺便说一下。。。O(n^2)不是最好的解决方案保罗·贝茨(Paul Betts)给了胖子比我更大的解决方案。但是我的仍然是LINQ解决方案,它比这里的其他解决方案更简单、更简短。我自己也在寻找类似的解决方案,最好不使用库或对整个列表进行排序。我的解决方案与问题本身类似,只是简化了一点
var min = People.Min(p => p.DateOfBirth);
var firstBorn = People.FirstOrDefault(p => p.DateOfBirth == min);
以下是更通用的解决方案。它本质上做了相同的事情(按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;
});
}
}
所以您要求的是
ArgMin
或ArgMax
。C#没有针对这些问题的内置API
我一直在寻找一种干净、高效(及时)的方法来做到这一点。我想我找到了一个:
这种模式的一般形式是:
var min = data.Select(x => (key(x), x)).Min().Item2;
^ ^ ^
the sorting key | take the associated original item
Min by key(.)
特别是,使用原始问题中的示例:
对于支持以下内容的C#7.0及以上版本:
var youngest = people.Select(p => (p.DateOfBirth, p)).Min().Item2;
var youngest = people.Select(p => new {age = p.DateOfBirth, ppl = p}).Min().ppl;
对于7.0之前的C#版本,可以改用:
var youngest = people.Select(p => (p.DateOfBirth, p)).Min().Item2;
var youngest = people.Select(p => new {age = p.DateOfBirth, ppl = p}).Min().ppl;
它们之所以有效,是因为值元组和匿名类型都有合理的默认比较器:对于(x1,y1)和(x2,y2),它首先比较x1
vsx2
,然后比较y1
vsy2
。这就是为什么内置的.Min
可以用于这些类型
由于匿名类型和值元组都是值类型,它们应该都非常有效
注意
在我上面的ArgMin
实现中,为了简单明了,我假设DateOfBirth
采用类型DateTime
。原始问题要求排除出生日期字段为空的条目:
Null DateOfBirth值被设置为DateTime.MaxValue,以便排除最小考虑因素(假设至少有一个具有指定的DOB)
它可以通过预滤波来实现
people.Where(p => p.DateOfBirth.HasValue)
因此,实现ArgMin
或ArgMax
的问题无关紧要
注2
上述方法有一个警告,即当有两个实例具有相同的min值时,min()
实现将尝试
people.Where(p => p.DateOfBirth.HasValue)
var youngest = Enumerable.Range(0, int.MaxValue)
.Zip(people, (idx, ppl) => (ppl.DateOfBirth, idx, ppl)).Min().Item3;
var firstBorn = People.Aggregate((min, x) => x.DateOfBirth < min.DateOfBirth ? x : min);
public static class IEnumerableExtensions
{
/// <summary>
/// Returns the element with the maximum value of a selector function.
/// </summary>
/// <typeparam name="TSource">The type of the elements of source.</typeparam>
/// <typeparam name="TKey">The type of the key returned by keySelector.</typeparam>
/// <param name="source">An IEnumerable collection values to determine the element with the maximum value of.</param>
/// <param name="keySelector">A function to extract the key for each element.</param>
/// <exception cref="System.ArgumentNullException">source or keySelector is null.</exception>
/// <exception cref="System.InvalidOperationException">source contains no elements.</exception>
/// <returns>The element in source with the maximum value of a selector function.</returns>
public static TSource MaxBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector) => MaxOrMinBy(source, keySelector, 1);
/// <summary>
/// Returns the element with the minimum value of a selector function.
/// </summary>
/// <typeparam name="TSource">The type of the elements of source.</typeparam>
/// <typeparam name="TKey">The type of the key returned by keySelector.</typeparam>
/// <param name="source">An IEnumerable collection values to determine the element with the minimum value of.</param>
/// <param name="keySelector">A function to extract the key for each element.</param>
/// <exception cref="System.ArgumentNullException">source or keySelector is null.</exception>
/// <exception cref="System.InvalidOperationException">source contains no elements.</exception>
/// <returns>The element in source with the minimum value of a selector function.</returns>
public static TSource MinBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector) => MaxOrMinBy(source, keySelector, -1);
private static TSource MaxOrMinBy<TSource, TKey>
(IEnumerable<TSource> source, Func<TSource, TKey> keySelector, int sign)
{
if (source == null) throw new ArgumentNullException(nameof(source));
if (keySelector == null) throw new ArgumentNullException(nameof(keySelector));
Comparer<TKey> comparer = Comparer<TKey>.Default;
TKey value = default(TKey);
TSource result = default(TSource);
bool hasValue = false;
foreach (TSource element in source)
{
TKey x = keySelector(element);
if (x != null)
{
if (!hasValue)
{
value = x;
result = element;
hasValue = true;
}
else if (sign * comparer.Compare(x, value) > 0)
{
value = x;
result = element;
}
}
}
if ((result != null) && !hasValue)
throw new InvalidOperationException("The source sequence is empty");
return result;
}
}
public class A
{
public int? a;
public A(int? a) { this.a = a; }
}
var b = a.MinBy(x => x.a);
var c = a.MaxBy(x => x.a);
var firstBornDate = People.GroupBy(p => p.DateOfBirth).Min(g => g.Key).FirstOrDefault();
public struct Money : IComparable<Money>
{
public Money(decimal value) : this() { Value = value; }
public decimal Value { get; private set; }
public int CompareTo(Money other) { return Value.CompareTo(other.Value); }
}
var amounts = new List<Money> { new Money(20), new Money(10) };
Money maxAmount = amounts.Max();
var amounts = new List<Money> { new Money(20), new Money(10) };
Money maxAmount = amounts.Min();
public static (double min, T obj) tMin<T>(this IEnumerable<T> ienum,
Func<T, double> aFunc)
{
var okNull = default(T);
if (okNull != null)
throw new ApplicationException("object passed to Min not nullable");
(double aMin, T okObj) best = (double.MaxValue, okNull);
foreach (T obj in ienum)
{
double q = aFunc(obj);
if (q < best.aMin)
best = (q, obj);
}
return (best);
}
(double okDist, Airport best) greatestPort = airPorts.tMin(x => x.dist(okLat, okLon));
var query = from person in People
where person.DateOfBirth!=null
orderby person.DateOfBirth
select person;
var firstBorn = query.Take(1).toList();