如何实现 MaxOrDefault(x => x.SomeInt) LINQ 扩展
本文关键字:LINQ 扩展 SomeInt MaxOrDefault 何实现 实现 | 更新日期: 2023-09-27 18:35:46
调用时。Max(x => x.SomeInt) 在 IEnumerable 上,如果枚举不包含任何元素,您通常很乐意返回"0"。但是 LINQ 的实现 .Max(x => x.SomeInt) 崩溃,因为序列不包含任何元素。
因此 .MaxOrDefault(x => x.SomeInt) 函数会很有用。
我们不应该简单地称.Any() 则 .Max(func),因为这会导致锐化器中出现合法的"可能的多重枚举"警告。
我已经实现了一个如下:
public static TResult MaxOrDefault<T, TResult>(this IEnumerable<T> enumerable, Func<T, TResult> func)
{
var list = enumerable.ToList();
if (!list.Any()) return default(TResult);
return list.Max(func);
}
但是,这样做的缺点是必须首先枚举到列表中,这是次优的,应该是不必要的。
有没有更好的方法?
以下是我认为更好/更整洁的实现:
public static TResult MaxOrDefault<T, TResult>(this IEnumerable<T> enumerable, Func<T, TResult> func)
{
return enumerable.Select(func).DefaultIfEmpty().Max();
}
DefaultIfEmpty
确保如果没有元素,则返回一个包含单个默认值的IEnumerable
,该默认值为 default(TResult)
,即数值类型为 0。
如果使用DefaultIfEmpty()
扩展名,如果序列为空,则始终可以保证至少有一个项目(默认项目)的序列。
var enumeration = ...;
var max = enumeration.DefaultIfEmpty().Max(x => ...);
如果你看一下微软的Max
实现,你可以看到它几乎已经做了你想要的,你需要做的就是更改最后一行代码以返回默认值,而不是抛出错误。
public static TSource MaxOrDefault<TSource>(this IEnumerable<TSource> source) {
if (source == null) throw Error.ArgumentNull("source");
Comparer<TSource> comparer = Comparer<TSource>.Default;
TSource value = default(TSource);
if (value == null) {
foreach (TSource x in source) {
if (x != null && (value == null || comparer.Compare(x, value) > 0))
value = x;
}
return value;
}
else {
bool hasValue = false;
foreach (TSource x in source) {
if (hasValue) {
if (comparer.Compare(x, value) > 0)
value = x;
}
else {
value = x;
hasValue = true;
}
}
return value;
}
}
如果您想使用选择器,只需为此复制重载即可。
public static TResult MaxOrDefault<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, TResult> selector) {
return MaxOrDefault(Enumerable.Select(source, selector));
}
任何现有的非通用版本都可以同样轻松地修改,只需替换最后一个
即可if (hasValue) return value;
throw Error.NoElements();
跟
return value;
利用这样一个事实,即MoveNext()
第一次调用时返回 false 意味着我们肯定处于默认情况,并且只需要遵循该路径,而我们还可以为下一次迭代做好准备:
public static TResult MaxOrDefault<T, TResult>(this IEnumerable<T> source, Func<T, TResult> func) where TResult : IComparable
{
if(source == null)
throw new ArgumentNullException("source");
using(var en = source.GetEnumerator())
if(en.MoveNext())
{
TResult max = func(en.Current);
while(en.MoveNext())
{
TResult cur = func(en.Current);
if(max == null || (cur != null && cur.CompareTo(max) > 0))
max = cur;
}
return max;
}
else
return default(TResult);
}
这虽然只处理IEnumerable
的情况(它处理其他IQueryable
类型,但只能将它们全部放入内存中)。如果我们希望它与所有可能的 Linq 源代码一起工作,我们希望从我们已经可以使用IQueryable
来构建我们的IQueryable
版本:
public static TResult MaxOrDefault<T, TResult>(this IQueryable<T> source, Func<T, TResult> func) where TResult : IComparable
{
if(source == null)
throw new ArgumentNullException("source");
return source.OrderByDescending(func).Select(func).FirstOrDefault();
}
这对于某些 Linq 源代码会更好,例如转换为适当的 SQL。
但是它比我们早期版本的内存中枚举效率低,但同时提供两者通常允许正常的重载选择更好的。