如何实现 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);
    }

但是,这样做的缺点是必须首先枚举到列表中,这是次优的,应该是不必要的。

有没有更好的方法?

如何实现 MaxOrDefault(x => x.SomeInt) LINQ 扩展

以下是我认为更好/更整洁的实现:

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。

但是它比我们早期版本的内存中枚举效率低,但同时提供两者通常允许正常的重载选择更好的。