在Linq实现"MinOrDefault"的最佳方式是什么?

Chr*_*son 76 c# linq

我正在从linq表达式生成一个十进制值列表,我想要最小的非零值.但是,linq表达式完全有可能导致空列表.

这将引发异常并且没有MinOrDefault来应对这种情况.

decimal result = (from Item itm in itemList
                  where itm.Amount > 0
                  select itm.Amount).Min();
Run Code Online (Sandbox Code Playgroud)

如果列表为空,将结果设置为0的最佳方法是什么?

Chr*_*tte 119

你想要的是这个:

IEnumerable<double> results = ... your query ...

double result = results.MinOrDefault();
Run Code Online (Sandbox Code Playgroud)

好吧,MinOrDefault()不存在.但是如果我们自己实现它会看起来像这样:

public static class EnumerableExtensions
{
    public static T MinOrDefault<T>(this IEnumerable<T> sequence)
    {
        if (sequence.Any())
        {
            return sequence.Min();
        }
        else
        {
            return default(T);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

但是,有一些功能System.Linq会产生相同的结果(以稍微不同的方式):

double result = results.DefaultIfEmpty().Min();
Run Code Online (Sandbox Code Playgroud)

如果results序列不包含任何元素,DefaultIfEmpty()则会产生一个包含一个元素的序列default(T)- 您随后可以调用该元素Min().

如果default(T)不是您想要的,那么您可以指定自己的默认值:

double myDefault = ...
double result = results.DefaultIfEmpty(myDefault).Min();
Run Code Online (Sandbox Code Playgroud)

现在,这很整洁!

  • 这里提到的MinOrDefault实现将遍历可枚举的两次.对于内存中的集合并不重要,但对于LINQ to Entity或延迟的"yield return"构建的枚举,这意味着两次往返数据库或两次处理第一个元素.我更喜欢results.DefaultIfEmpty(myDefault).Min()解决方案. (15认同)
  • 查看`DefaultIfEmpty`的源代码,它确实是智能实现的,只有在使用`yield return`s的元素时才转发序列. (4认同)
  • @JDandChips 你引用的形式是 `DefaultIfEmpty`,它采用 `IEnumerable&lt;T&gt;`。如果你在一个 `IQueryable&lt;T&gt;` 上调用它,比如你会在数据库操作中调用它,那么它不会返回一个单例序列,而是生成一个适当的 `MethodCallExpression`,因此结果查询不需要一切被检索。不过,这里建议的“EnumerableExtensions”方法确实存在这个问题。 (2认同)

Mar*_*ell 52

decimal? result = (from Item itm in itemList
                  where itm.Amount != 0
                  select (decimal?)itm.Amount).Min();
Run Code Online (Sandbox Code Playgroud)

请注意转换为decimal?.如果没有,你会得到一个空的结果(只是在事后处理 - 我主要说明如何停止异常).我也做了"非零"使用!=而不是>.

  • 试试看:`十进制?result =(new decimal?[0]).Min();`给`null` (7认同)
  • 然后可能会使用?0得到想要的结果? (2认同)

Jon*_*nna 10

正如已经提到的那样,在少量代码中执行一次最好的方法是:

decimal result = (from Item itm in itemList
  where itm.Amount > 0
    select itm.Amount).DefaultIfEmpty().Min();
Run Code Online (Sandbox Code Playgroud)

随着铸造itm.Amountdecimal?并获得Min那是最整洁的,如果我们希望能够检测到这种空状态.

如果你想要实际提供一个,MinOrDefault()那么我们当然可以从:

public static TSource MinOrDefault<TSource>(this IQueryable<TSource> source, TSource defaultValue)
{
  return source.DefaultIfEmpty(defaultValue).Min();
}

public static TSource MinOrDefault<TSource>(this IQueryable<TSource> source)
{
  return source.DefaultIfEmpty(defaultValue).Min();
}

public static TResult MinOrDefault<TSource, TResult>(this IQueryable<TSource> source, Expression<Func<TSource, TResult>> selector, TSource defaultValue)
{
  return source.DefaultIfEmpty(defaultValue).Min(selector);
}

public static TResult MinOrDefault<TSource, TResult>(this IQueryable<TSource> source, Expression<Func<TSource, TResult>> selector)
{
  return source.DefaultIfEmpty().Min(selector);
}
Run Code Online (Sandbox Code Playgroud)

您现在拥有一整套MinOrDefault是否包含选择器,以及是否指定默认值.

从这一点开始,您的代码就是:

decimal result = (from Item itm in itemList
  where itm.Amount > 0
    select itm.Amount).MinOrDefault();
Run Code Online (Sandbox Code Playgroud)

所以,虽然它不是那么开始,但从那时起它就更整洁了.

可是等等!还有更多!

假设您使用EF并希望使用async支持.轻松完成:

public static Task<TSource> MinOrDefaultAsync<TSource>(this IQueryable<TSource> source, TSource defaultValue)
{
  return source.DefaultIfEmpty(defaultValue).MinAsync();
}

public static Task<TSource> MinOrDefaultAsync<TSource>(this IQueryable<TSource> source)
{
  return source.DefaultIfEmpty(defaultValue).MinAsync();
}

public static Task<TSource> MinOrDefaultAsync<TSource, TResult>(this IQueryable<TSource> source, Expression<Func<TSource, TResult>> selector, TSource defaultValue)
{
  return source.DefaultIfEmpty(defaultValue).MinAsync(selector);
}

public static Task<TSource> MinOrDefaultAsync<TSource, TResult>(this IQueryable<TSource> source, Expression<Func<TSource, TResult>> selector)
{
  return source.DefaultIfEmpty().MinAsync(selector);
}
Run Code Online (Sandbox Code Playgroud)

(注意我不在await这里使用;我们可以直接创建一个Task<TSource>没有它我们需要的东西,从而避免隐藏的复杂性await带来).

但等等,还有更多!假设我们IEnumerable<T>有时会使用它.我们的方法是次优的.当然,我们可以做得更好!

首先,Min定义上int?,long?,float? double?decimal?已经做我们想做(就像马克Gravell的答案利用的).同样,我们也从Min已定义的行为中获取我们想要的行为,如果被调用的话T?.因此,让我们做一些小的,因此很容易内联的方法来利用这个事实:

public static TSource? MinOrDefault<TSource>(this IEnumerable<TSource?> source, TSource? defaultValue) where TSource : struct
{
  return source.Min() ?? defaultValue;
}
public static TSource? MinOrDefault<TSource>(this IEnumerable<TSource?> source) where TSource : struct
{
  return source.Min();
}
public static TResult? Min<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, TResult?> selector, TResult? defaultValue) where TResult : struct
{
  return source.Min(selector) ?? defaultValue;
}
public static TResult? Min<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, TResult?> selector) where TResult : struct
{
  return source.Min(selector);
}
Run Code Online (Sandbox Code Playgroud)

现在让我们先从更一般的案例开始:

public static TSource MinOrDefault<TSource>(this IEnumerable<TSource> source, TSource defaultValue)
{
  if(default(TSource) == null) //Nullable type. Min already copes with empty sequences
  {
    //Note that the jitter generally removes this code completely when `TSource` is not nullable.
    var result = source.Min();
    return result == null ? defaultValue : result;
  }
  else
  {
    //Note that the jitter generally removes this code completely when `TSource` is nullable.
    var comparer = Comparer<TSource>.Default;
    using(var en = source.GetEnumerator())
      if(en.MoveNext())
      {
        var currentMin = en.Current;
        while(en.MoveNext())
        {
          var current = en.Current;
          if(comparer.Compare(current, currentMin) < 0)
            currentMin = current;
        }
        return currentMin;
      }
  }
  return defaultValue;
}
Run Code Online (Sandbox Code Playgroud)

现在显而易见的覆盖使用了这个:

public static TSource MinOrDefault<TSource>(this IEnumerable<TSource> source)
{
  var defaultValue = default(TSource);
  return defaultValue == null ? source.Min() : source.MinOrDefault(defaultValue);
}
public static TResult MinOrDefault<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, TResult> selector, TResult defaultValue)
{
  return source.Select(selector).MinOrDefault(defaultValue);
}
public static TResult MinOrDefault<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, TResult> selector)
{
  return source.Select(selector).MinOrDefault();
}
Run Code Online (Sandbox Code Playgroud)

如果我们真的看好性能,我们可以针对某些情况进行优化,就像这样Enumerable.Min():

public static int MinOrDefault(this IEnumerable<int> source, int defaultValue)
{
  using(var en = source.GetEnumerator())
    if(en.MoveNext())
    {
      var currentMin = en.Current;
      while(en.MoveNext())
      {
        var current = en.Current;
        if(current < currentMin)
          currentMin = current;
      }
      return currentMin;
    }
  return defaultValue;
}
public static int MinOrDefault(this IEnumerable<int> source)
{
  return source.MinOrDefault(0);
}
public static int MinOrDefault<TSource>(this IEnumerable<TSource> source, Func<TSource, int> selector, int defaultValue)
{
  return source.Select(selector).MinOrDefault(defaultValue);
}
public static int MinOrDefault<TSource>(this IEnumerable<TSource> source, Func<TSource, int> selector)
{
  return source.Select(selector).MinOrDefault();
}
Run Code Online (Sandbox Code Playgroud)

等了long,float,doubledecimal相匹配的一套Min()提供Enumerable.这是T4模板很有用的东西.

最后MinOrDefault(),对于各种类型的我们来说,我们几乎可以实现我们所希望的实施.面对一次使用它当然不是"整洁"(再次,只是使用DefaultIfEmpty().Min()),但如果我们发现自己经常使用它,那么非常"整洁",所以我们有一个很好的库我们可以重用(或者实际上,粘贴到StackOverflow上的答案......).