我有一个带有 Nullable DateOfBirth 属性的 Person 对象。有没有一种方法可以使用 LINQ 来查询 Person 对象列表中具有最早/最小 DateOfBirth 值的对象?
这是我开始的:
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);
还是有更精简的方法?
a.Min(x => x.foo);
max("find a word of maximal length in this sentence".split(), key=len)
返回字符串 'sentence'。在 C# 中,"find a word of maximal length in this sentence".Split().Max(word => word.Length)
计算出 8 是任何单词的最长长度,但不会告诉您最长的单词 是什么。
People.Aggregate((curMin, x) => (curMin == null || (x.DateOfBirth ?? DateTime.MaxValue) <
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;
}
}
示例用法:
var firstBorn = People.MinBy(p => p.DateOfBirth ?? DateTime.MaxValue);
请注意,如果序列为空,这将引发异常,如果有多个,则返回具有最小值的第一个元素。
或者,您可以在 MinBy.cs 中使用我们在 MoreLINQ 中获得的实现。 (当然,有一个对应的 MaxBy
。)
通过包管理器控制台安装:
PM> Install-Package morelinq
注意:为了完整起见,我包含了这个答案,因为 OP 没有提到数据源是什么,我们不应该做出任何假设。
此查询给出了正确答案,但可能会更慢,因为它可能必须对 People
中的所有项进行排序,具体取决于 People
的数据结构:
var oldest = People.OrderBy(p => p.DateOfBirth ?? DateTime.MaxValue).First();
更新:实际上我不应该将此解决方案称为“幼稚”,但用户确实需要知道他在查询什么。该解决方案的“缓慢性”取决于基础数据。如果这是一个数组或 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>
之类的排序集合创建特殊情况,但据我所知,它没有。
People.OrderBy(p => p.DateOfBirth.GetValueOrDefault(DateTime.MaxValue)).First()
会做的伎俩
所以你要的是 ArgMin
或 ArgMax
。 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(.)
特别是,使用原始问题中的示例:
对于支持 value tuple 的 C# 7.0 及更高版本:
var youngest = people.Select(p => (p.DateOfBirth, p)).Min().Item2;
对于 7.0 之前的 C# 版本,可以使用 anonymous type 代替:
var youngest = people.Select(p => new {age = p.DateOfBirth, ppl = p}).Min().ppl;
它们之所以起作用,是因为值元组和匿名类型都有合理的默认比较器:对于 (x1, y1) 和 (x2, y2),它首先比较 x1
与 x2
,然后是 y1
与 y2
。这就是为什么可以在这些类型上使用内置 .Min
的原因。
而且由于匿名类型和值元组都是值类型,因此它们都应该非常有效。
笔记
在我上面的 ArgMin
实现中,为了简单明了,我假设 DateOfBirth
采用类型 DateTime
。原始问题要求排除具有 null DateOfBirth
字段的条目:
Null DateOfBirth 值设置为 DateTime.MaxValue 以便将它们排除在 Min 考虑之外(假设至少有一个具有指定的 DOB)。
它可以通过预过滤来实现
people.Where(p => p.DateOfBirth.HasValue)
因此,实现 ArgMin
或 ArgMax
的问题无关紧要。
笔记2
上述方法有一个警告,即当有两个实例具有相同的最小值时,Min()
实现将尝试将实例作为决胜局进行比较。但是,如果实例的类没有实现 IComparable
,则会抛出运行时错误:
至少一个对象必须实现 IComparable
幸运的是,这仍然可以相当干净地修复。这个想法是将一个遥远的“ID”与作为明确的决胜局的每个条目相关联。我们可以为每个条目使用增量 ID。仍然以人的年龄为例:
var youngest = Enumerable.Range(0, int.MaxValue)
.Zip(people, (idx, ppl) => (ppl.DateOfBirth, idx, ppl)).Min().Item3;
var (minDateOfBirth, idx, youngestPerson) = people.Select((p, idx) => (p.DateOfBirth, idx, p)).Min()
没有额外包的解决方案:
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();
}
}
在这种情况下:
var min = lst.MinBy(i => i.StartDate);
var max = lst.MaxBy(i => i.StartDate);
顺便说一句... O(n^2) 不是最好的解决方案。 Paul Betts 给出了比我更胖的解决方案。但我的仍然是 LINQ 解决方案,它比这里的其他解决方案更简单、更短。
从 .Net 6(Preview 7)或更高版本开始,有新的内置方法 Enumerable.MaxBy 和 Enumerable.MinBy 来实现这一点。
var lastBorn = people.MaxBy(p => p.DateOfBirth);
var firstBorn = people.MinBy(p => p.DateOfBirth);
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();
}
完全简单的聚合使用(相当于其他语言的折叠):
var firstBorn = People.Aggregate((min, x) => x.DateOfBirth < min.DateOfBirth ? x : min);
唯一的缺点是每个序列元素访问该属性两次,这可能很昂贵。这很难解决。
你可以像 SQL 中的 order by 和 limit/fetch 一样来做。因此,您按 DateOfBirth 升序排序,然后仅获取第一行。
var query = from person in People
where person.DateOfBirth!=null
orderby person.DateOfBirth
select person;
var firstBorn = query.Take(1).toList();
OrderBy
+ FirstOrDefault
相同,因此该答案并没有真正添加任何新内容。此外,只有 'Skip` + Take
翻译为限制/获取。 Take(1)
翻译为 TOP(1)。这是关于 LINQ 到对象,而不是 LINQ 到 SQL 后端。
以下是更通用的解决方案。它本质上做同样的事情(以 O(N) 顺序),但在任何 IEnumerable 类型上,并且可以与属性选择器可以返回 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
尝试以下想法:
var firstBornDate = People.GroupBy(p => p.DateOfBirth).Min(g => g.Key).FirstOrDefault();
我自己也在寻找类似的东西,最好不使用库或对整个列表进行排序。我的解决方案最终类似于问题本身,只是简化了一点。
var min = People.Min(p => p.DateOfBirth);
var firstBorn = People.FirstOrDefault(p => p.DateOfBirth == min);
var min = People.Min(...); var firstBorn = People.FirstOrDefault(p => p.DateOfBirth == min...
否则,它会反复获取最小值,直到找到您要查找的那个。
再次编辑:
对不起。除了缺少可空值之外,我还查看了错误的函数,
Min<(Of <(TSource, TResult>)>)(IEnumerable<(Of <(TSource>)>), Func<(Of <(TSource, TResult>)>)) 确实返回了您所说的结果类型。
我想说一种可能的解决方案是实现 IComparable 并使用 Min<(Of <(TSource>)>)(IEnumerable<(Of <(TSource>)>)),它确实会从 IEnumerable 返回一个元素。当然,如果您无法修改元素,那将无济于事。我觉得 MS 的设计在这里有点奇怪。
当然,如果需要,您可以随时执行 for 循环,或者使用 Jon Skeet 提供的 MoreLINQ 实现。
另一种实现,它可以使用可为空的选择器键,并且对于引用类型的集合,如果没有找到合适的元素,则返回 null。例如,这可能有助于处理数据库结果。
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);
如果您想选择具有最小或最大属性值的对象。另一种方法是使用实现 IComparable。
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();
这样,您可以在返回对象类型的同时比较任何对象并获取 Max 和 Min。
希望这会对某人有所帮助。
一种通过 IEnumerable 上的扩展函数返回对象和找到的最小值的方法。它需要一个可以对集合中的对象执行任何操作的 Func:
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);
}
对象是机场的示例,我们希望找到离给定(纬度、经度)最近的机场。机场具有 dist(lat, lon) 功能。
(double okDist, Airport best) greatestPort = airPorts.tMin(x => x.dist(okLat, okLon));
这是获取最小值和最大值的简单方法:
`dbcontext.tableName.Select(x=>x.Feild1).Min()`
您可以使用现有的 linq 扩展,例如 MoreLinq。但是如果你只需要这些方法,那么你可以在这里使用简单的代码:
public static IEnumerable<T> MinBys<T>(this IEnumerable<T> collection, Func<T, IComparable> selector)
{
var dict = collection.GroupBy(selector).ToDictionary(g => g.Key);
return dict[dict.Keys.Min()];
}
public static IEnumerable<T> MaxBys<T>(this IEnumerable<T> collection, Func<T, IComparable> selector)
{
var dict = collection.GroupBy(selector).ToDictionary(g => g.Key);
return dict[dict.Keys.Max()];
}
selector
产生可比较的类型时,才能使用 Min 和 Max。
selector
返回一个匿名类型。
where TVal: IComparable
,它会有用吗?
curMin == null
?如果您将Aggregate()
与null
的种子一起使用,则curMin
只能是null
。