Cách gọn gàng nhất để đạt được “MinOrDefault” trong Linq là gì?


82

Tôi đang tạo danh sách các giá trị thập phân từ biểu thức linq và tôi muốn giá trị nhỏ nhất khác 0. Tuy nhiên, hoàn toàn có thể là biểu thức linq sẽ dẫn đến một danh sách trống.

Điều này sẽ tạo ra một ngoại lệ và không có MinOrDefault để đối phó với tình huống này.

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

Cách gọn gàng nhất để đặt kết quả thành 0 nếu danh sách trống là gì?


9
+1 để đề xuất thêm MinOrDefault () vào thư viện.
J. Andrew Laughlin

Câu trả lời:


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

Lưu ý chuyển đổi thành decimal?. Bạn sẽ nhận được kết quả trống nếu không có kết quả nào (chỉ cần xử lý điều đó sau khi thực tế - tôi chủ yếu minh họa cách dừng ngoại lệ). Tôi cũng sử dụng "khác không" !=hơn là >.


hấp dẫn. Tôi không thể làm việc ra làm thế nào điều này sẽ tránh một danh sách rỗng nhưng tôi sẽ cung cấp cho nó một thử
Chris Simpson

7
Hãy thử: decimal? result = (new decimal?[0]).Min();chonull
Marc Gravell

2
và có lẽ sau đó sử dụng ?? 0 để có được kết quả mong muốn?
Christoffer Lette vào

Nó chắc chắn hoạt động. Tôi vừa tạo một bài kiểm tra đơn vị để thử nó, nhưng tôi sẽ phải mất 5 phút để tìm ra lý do tại sao kết quả của lựa chọn là một giá trị null duy nhất chứ không phải là một danh sách trống (có thể nền sql của tôi khiến tôi bối rối ). Cám ơn vì cái này.
Chris Simpson

1
@Lette, nếu tôi thay đổi nó thành: decimal result1 = ..... Min () ?? 0; điều này cũng hoạt động, vì vậy cảm ơn bạn đã đóng góp ý kiến.
Chris Simpson

125

Những gì bạn muốn là:

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

double result = results.MinOrDefault();

Vâng, MinOrDefault()không tồn tại. Nhưng nếu chúng tôi tự thực hiện nó, nó sẽ trông giống như sau:

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

Tuy nhiên, có một chức năng System.Linqsẽ tạo ra cùng một kết quả (theo một cách hơi khác):

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

Nếu resultschuỗi không chứa phần tử, DefaultIfEmpty()sẽ tạo ra một chuỗi chứa một phần tử - phần tử default(T)- mà sau đó bạn có thể gọi Min().

Nếu default(T)không phải là những gì bạn muốn, thì bạn có thể chỉ định mặc định của riêng mình với:

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

Bây giờ, đó là gọn gàng!


1
@ChristofferLette Tôi chỉ muốn một danh sách T trống, vì vậy tôi cũng đã kết thúc sử dụng Any () với Min (). Cảm ơn!
Adrian Marinica

1
@AdrianMar: BTW, bạn có cân nhắc sử dụng Null Object làm mặc định không?
Christoffer Lette

17
Việc triển khai MinOrDefault được đề cập ở đây sẽ lặp lại hai lần trong danh sách. Điều này không quan trọng đối với các bộ sưu tập trong bộ nhớ, nhưng đối với LINQ to Entity hoặc các liệt kê được xây dựng "trả về lợi nhuận" lười biếng, điều này có nghĩa là hai lần đi vòng lại cơ sở dữ liệu hoặc xử lý phần tử đầu tiên hai lần. Tôi thích giải pháp kết quả.DefaultIfEmpty (myDefault) .Min ().
Kevin Coulombe.

4
Nhìn vào nguồn cho DefaultIfEmpty, nó thực sự được triển khai thông minh, chỉ chuyển tiếp chuỗi nếu có phần tử sử dụng yield returns.
Peter Lillevold

2
@JDandChips mà bạn đang trích dẫn từ dạng DefaultIfEmptyIEnumerable<T>. Nếu bạn đã gọi nó trên một IQueryable<T>, chẳng hạn như bạn sẽ làm với một hoạt động cơ sở dữ liệu, thì nó không trả về một chuỗi singleton, mà tạo ra một chuỗi thích hợp MethodCallExpression, và do đó, truy vấn kết quả không yêu cầu mọi thứ được truy xuất. Tuy nhiên, EnumerableExtensionscách tiếp cận được đề xuất ở đây có vấn đề đó.
Jon Hanna

16

Điều gọn gàng nhất về việc chỉ thực hiện một lần với một mã số tiền nhỏ, như đã đề cập:

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

Với việc truyền itm.Amountđến decimal?và lấy được Minđiều đó là gọn gàng nhất nếu chúng ta muốn có thể phát hiện điều kiện trống này.

Tuy nhiên, nếu bạn thực sự muốn cung cấp MinOrDefault()thì tất nhiên chúng ta có thể bắt đầu với:

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

Bây giờ bạn có một tập hợp đầy đủ về MinOrDefaultviệc bạn có bao gồm bộ chọn hay không và bạn có chỉ định mặc định hay không.

Từ thời điểm này, mã của bạn chỉ đơn giản là:

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

Vì vậy, mặc dù nó không gọn gàng như ban đầu, nhưng từ đó nó sẽ gọn gàng hơn.

Nhưng đợi đã! Còn nữa!

Giả sử bạn sử dụng EF và muốn sử dụng asynchỗ trợ. Dễ dàng thực hiện:

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

(Lưu ý rằng tôi không sử dụng awaitở đây; chúng ta có thể trực tiếp tạo ra một Task<TSource>cái mà chúng ta cần mà không cần nó, và do đó tránh được những phức tạp tiềm ẩn awaitmang lại).

Nhưng xin chờ chút nữa! Giả sử chúng tôi đang sử dụng điều này với IEnumerable<T>một số lần. Cách tiếp cận của chúng tôi là không tối ưu. Chắc chắn chúng tôi có thể làm tốt hơn!

Thứ nhất, Minxác định trên int?, long?, float? double?decimal?đã làm những gì chúng muốn anyway (như làm cho câu trả lời Marc Gravell của việc sử dụng). Tương tự, chúng tôi cũng nhận được hành vi chúng tôi muốn từ hành vi Minđã được xác định nếu được gọi cho bất kỳ hành vi nào khác T?. Vì vậy, chúng ta hãy thực hiện một số phương pháp nhỏ, và do đó dễ dàng nội dung, để tận dụng thực tế này:

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

Bây giờ hãy bắt đầu với trường hợp tổng quát hơn trước:

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

Bây giờ các ghi đè rõ ràng sử dụng điều này:

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

Nếu chúng tôi thực sự lạc quan về hiệu suất, chúng tôi có thể tối ưu hóa cho một số trường hợp nhất định, giống như 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();
}

Và vân vân cho long, float, doubledecimalđể phù hợp với bộ Min()cung cấp bởi Enumerable. Đây là loại mà các mẫu T4 hữu ích.

Cuối cùng, chúng tôi có một triển khai hiệu quả MinOrDefault()như chúng tôi có thể hy vọng, cho một loạt các loại. Chắc chắn là không "gọn gàng" khi sử dụng một lần cho nó (một lần nữa, chỉ sử dụng DefaultIfEmpty().Min()), nhưng rất "gọn gàng" nếu chúng ta thấy mình sử dụng nó nhiều, vì vậy chúng ta có một thư viện đẹp, chúng ta có thể sử dụng lại (hoặc thực sự, dán vào câu trả lời trên StackOverflow…).


0

Cách tiếp cận này sẽ trả về Amountgiá trị nhỏ nhất duy nhất từ itemList. Về lý thuyết, điều này nên tránh nhiều chuyến đi khứ hồi đến cơ sở dữ liệu.

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

Ngoại lệ tham chiếu null không còn được gây ra vì chúng tôi đang sử dụng kiểu nullable.

Bằng cách tránh sử dụng các phương thức thực thi như Anytrước khi gọi Min, chúng ta chỉ nên thực hiện một chuyến đi đến cơ sở dữ liệu


1
Điều gì khiến bạn nghĩ rằng việc sử dụng Selecttrong câu trả lời được chấp nhận sẽ thực hiện truy vấn nhiều hơn một lần? Câu trả lời được chấp nhận sẽ dẫn đến một lệnh gọi DB.
Jon Hanna

Bạn nói đúng, Selectlà một phương pháp hoãn lại và sẽ không gây ra thực thi. Tôi đã loại bỏ những lời nói dối này khỏi câu trả lời của mình. Tham khảo: "Pro ASP.NET MVC4" của Adam Freeman (Sách)
JDandChips

Nếu bạn muốn thực sự lạc quan trong việc đảm bảo không có sự lãng phí, hãy xem câu trả lời mà tôi vừa đăng.
Jon Hanna

-1

Nếu itemList không thể nullable (trong đó DefaultIfEmpty cho 0) và bạn muốn null làm giá trị đầu ra tiềm năng, bạn cũng có thể sử dụng cú pháp lambda:

decimal? result = itemList.Where(x => x.Amount != 0).Min(x => (decimal?)x);
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.