在Linq中实现“ MinOrDefault”的最新方法是什么?


82

我从linq表达式生成一个十进制值的列表,我想要最小的非零值。但是,linq表达式很有可能会导致一个空列表。

这将引发异常,并且没有MinOrDefault可以应对这种情况。

decimal result = (from Item itm in itemList
                  where itm.Amount > 0
                  select itm.Amount).Min();

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


9
+1,建议将MinOrDefault()添加到库中。
J.安德鲁·劳克林(Andrew Laughlin)2013年

Answers:


54
decimal? result = (from Item itm in itemList
                  where itm.Amount != 0
                  select (decimal?)itm.Amount).Min();

注意转换为decimal?。如果没有结果,您将得到一个空结果(在事实发生之后就进行处理-我主要是在说明如何停止异常)。我还使用了“非零”!=而不是>


有趣。我无法弄清楚如何避免出现空列表,但我会尝试一下
克里斯·辛普森

7
尝试:decimal? result = (new decimal?[0]).Min();给予null
马克·格雷韦尔

2
然后也许使用?? 0以获得理想的结果?
Christoffer Lette 2010年

绝对可以。我刚刚构建了一个单元测试来进行尝试,但是我将不得不花5分钟来弄清为什么选择结果是单个null值而不是一个空列表(这可能是我的sql背景使我感到困惑的原因) )。谢谢你
克里斯·辛普森

1
@Lette,如果我将其更改为:十进制result1 = ..... Min()?0; 这也有效,因此感谢您的输入。
克里斯·辛普森

125

您想要的是:

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

double result = results.MinOrDefault();

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

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

但是,其中的功能System.Linq会产生相同的结果(以略有不同的方式):

double result = results.DefaultIfEmpty().Min();

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

如果default(T)不是您想要的,则可以使用以下命令指定自己的默认值:

double myDefault = ...
double result = results.DefaultIfEmpty(myDefault).Min();

现在,这很干净!


1
@ChristofferLette我只需要一个空的T列表,所以我最终也将Min()与Any()一起使用。谢谢!
阿德里安·马里尼卡

1
@AdrianMar:顺便说一句,您是否考虑使用Null对象作为默认对象
Christoffer Lette

17
这里提到的MinOrDefault实现将对可枚举进行两次迭代。对于内存中的集合而言,这无关紧要,但是对于LINQ to Entity或惰性的“ yield return”构建的枚举而言,这意味着两次到数据库的往返或两次处理第一个元素。我更喜欢result.DefaultIfEmpty(myDefault).Min()解决方案。
凯文·

4
从的源头DefaultIfEmpty来看,确实实现了聪明,只有在存在使用yield returns的元素的情况下,才转发序列。
Peter Lillevold 2014年

2
@JDandChips你从形式引用的DefaultIfEmpty接受一个IEnumerable<T>。如果您在上调用了它IQueryable<T>,例如使用数据库操作时那样,则它不会返回单例序列,而是会生成适当的序列,MethodCallExpression因此生成的查询不需要检索所有内容。EnumerableExtensions尽管这里建议的方法确实存在该问题。
乔恩·汉纳

16

如前所述,最简单的方法是只用少量代码执行一次:

decimal result = (from Item itm in itemList
  where itm.Amount > 0
    select itm.Amount).DefaultIfEmpty().Min();

如果我们希望能够检测到这种空状态,则将其强制转换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);
}

现在,您可以确定MinOrDefault是否包含选择器,以及是否指定默认选择器。

从这一点来看,您的代码很简单:

decimal result = (from Item itm in itemList
  where itm.Amount > 0
    select itm.Amount).MinOrDefault();

因此,虽然一开始并不那么整洁,但从那时起它变得更加整洁。

可是等等!还有更多!

假设您使用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);
}

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

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

首先,Min定义上int?long?float? double?decimal?已经做我们想做(就像马克Gravell的答案利用的)。同样,Min如果调用了other ,我们也可以从已经定义的行为中获得想要的行为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);
}

现在让我们先从更一般的情况开始:

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;
}

现在,明显的替代品利用了这一点:

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();
}

如果我们真的很看好性能,我们可以针对某些情况进行优化,就像这样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();
}

等了longfloatdoubledecimal相匹配的一套Min()提供Enumerable。T4模板很有用。

最后MinOrDefault(),对于各种各样的类型,我们几乎可以实现我们所期望的性能。当然,面对它的一种用法(再次使用DefaultIfEmpty().Min())并不是“整洁”的,但是如果我们发现自己经常使用它,那么就非常“整洁”,因此我们有一个很好的库可以重用(或者确实粘贴到关于StackOverflow的答案...)。


0

这种方法会返回一个最小Amount的值itemList。从理论上讲,这应该避免多次往返数据库。

decimal? result = (from Item itm in itemList
                  where itm.Amount > 0)
                 .Min(itm => (decimal?)itm.Amount);

因为我们使用的是可空类型,所以不再导致空引用异常。

通过避免使用诸如Any调用之前的执行方法Min,我们应该只访问数据库一次


1
是什么让您认为Select在接受的答案中使用会多次执行查询?接受的答案将导致单个数据库调用。
乔恩·汉纳

没错,这Select是一个延迟的方法,不会导致执行。我已经从答案中删除了这些谎言。参考:Adam Freeman撰写的“ Pro ASP.NET MVC4”(本书)
JDandChips,2015年

如果您真的想确保没有浪费,请看一下我刚刚发布的答案。
乔恩·汉纳

-1

如果itemList是不可为空的(其中DefaultIfEmpty为0),并且您希望将null作为潜在的输出值,则也可以使用lambda语法:

decimal? result = itemList.Where(x => x.Amount != 0).Min(x => (decimal?)x);
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.