如何在 LINQ 中实现“MinOrDefault”?

Posted

技术标签:

【中文标题】如何在 LINQ 中实现“MinOrDefault”?【英文标题】:How to achieve "MinOrDefault" in LINQ? 【发布时间】:2011-01-11 01:23:13 【问题描述】:

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

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

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

如果列表为空,如何将结果设置为 0?

【问题讨论】:

【参考方案1】:

你想要的是这个:

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

现在,这很整洁!

【讨论】:

@ChristofferLette 我只想要一个 T 的空列表,所以我最终也使用了带有 Min() 的 Any()。谢谢! @AdrianMar:顺便说一句,你考虑过使用Null Object 作为默认值吗? 这里提到的 MinOrDefault 实现将遍历可枚举两次。内存中的集合无关紧要,但对于 LINQ to Entity 或惰性“yield return”内置枚举,这意味着两次往返数据库或处理第一个元素两次。我更喜欢 results.DefaultIfEmpty(myDefault).Min() 解决方案。 DefaultIfEmpty的源码,确实是智能实现的,只有在有元素使用yield returns时才会转发序列。 @JDandChips 您引用的格式为DefaultIfEmpty,采用IEnumerable&lt;T&gt;。如果您在IQueryable&lt;T&gt; 上调用它,就像您在数据库操作中那样,那么它不会返回单例序列,而是生成适当的MethodCallExpression,因此生成的查询不需要检索所有内容。不过,这里建议的 EnumerableExtensions 方法确实存在这个问题。【参考方案2】:
decimal? result = (from Item itm in itemList
                  where itm.Amount != 0
                  select (decimal?)itm.Amount).Min();

注意转换为decimal?。如果没有结果,您将得到一个空结果(事后处理 - 我主要说明如何停止异常)。我还“非零”使用!= 而不是&gt;

【讨论】:

有趣。我不知道这将如何避免空列表,但我会试一试 试试看:decimal? result = (new decimal?[0]).Min(); 给了null 也许然后使用 ?? 0 得到想要的结果? 绝对有效。我刚刚建立了一个单元测试来尝试它,但我将不得不花 5 分钟来弄清楚为什么选择的结果是一个空值而不是一个空列表(我的 sql 背景可能让我感到困惑)。谢谢你。 @Lette,如果我将其更改为:decimal result1 = .....Min() ?? 0;这也有效,所以感谢您的意见。【参考方案3】:

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

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

itm.Amount 转换为decimal? 并获得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&lt;TSource&gt;,没有它就可以满足我们的需要,从而避免await带来的隐藏复杂性)。

但是等等,还有更多!假设我们有时将它与IEnumerable&lt;T&gt; 一起使用。我们的方法是次优的。我们当然可以做得更好!

首先,int?long?float? double?decimal? 上定义的 Min 无论如何都已经做了我们想要的(正如 Marc Gravell 的回答所使用的那样)。同样,如果调用任何其他T?,我们也可以从已经定义的Min 中获得我们想要的行为。所以让我们做一些小的,因此很容易内联的方法来利用这个事实:

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 以此类推,以匹配 Enumerable 提供的 Min() 集合。这就是 T4 模板有用的地方。

最后,我们的MinOrDefault() 实现的性能几乎与我们希望的一样,适用于各种类型。面对它的一个用途,当然不是“整洁”(再次,只需使用DefaultIfEmpty().Min()),但如果我们发现自己经常使用它,那就非常“整洁”,所以我们有一个很好的库可以重用(或者实际上,粘贴到 *** 上的答案中……)。

【讨论】:

【参考方案4】:

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

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

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

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

【讨论】:

是什么让您认为在接受的答案中使用Select 会多次执行查询?接受的答案将导致单个数据库调用。 你说得对,Select 是一种延迟方法,不会导致执行。我已经从我的回答中删除了这些谎言。参考:Adam Freeman 的“Pro ASP.NET MVC4”(书籍) 如果您想真正看好确保没有浪费,请查看我刚刚发布的答案。【参考方案5】:
decimal result;
try
  result = (from Item itm in itemList
                  where itm.Amount != 0
                  select (decimal?)itm.Amount).Min();
catch(Exception e)
  result = 0;

【讨论】:

【参考方案6】:

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

decimal? result = itemList.Where(x => x.Amount != 0).Min(x => (decimal?)x);

【讨论】:

以上是关于如何在 LINQ 中实现“MinOrDefault”?的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 LINQ 在 C# 中实现梯度下降算法?

如何在 Linq to SQL 中实现缓存?

如何使用 Entity Framework Core 2.0 上的 lambda 语法在 LINQ 中实现 LEFT OUTER JOIN?

有没有啥简单的方法可以在 Linq 中实现 SQL Server 的合并查询?

在 C# LINQ 中实现 RANK OVER SQL 子句

c#.net 在LINQ中实现取数据库中最大值