Linq'te "MinOrDefault" elde etmenin en düzgün yolu nedir?


82

Bir linq ifadesinden ondalık değerlerin bir listesini oluşturuyorum ve minimum sıfır olmayan değeri istiyorum. Bununla birlikte, linq ifadesinin boş bir liste ile sonuçlanması tamamen mümkündür.

Bu bir istisna yaratacaktır ve bu durumla başa çıkmak için MinOrDefault yoktur.

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

Liste boşsa sonucu 0 olarak ayarlamanın en düzgün yolu nedir?


9
MinOrDefault () 'un kitaplığa eklenmesini önermek için +1.
J.Andrew Laughlin

Yanıtlar:


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

Dönüştürmeye dikkat edin decimal?. Hiçbiri yoksa boş bir sonuç alırsınız (sadece bunu hallettikten sonra - ben esas olarak istisnayı nasıl durduracağımı gösteriyorum). Bunun !=yerine "sıfır olmayan" bir kullanım da yaptım >.


ilginç. Bunun boş bir listeden nasıl kaçınacağını bilemiyorum ama bir deneyeceğim
Chris Simpson

7
Deneyin: decimal? result = (new decimal?[0]).Min();verirnull
Marc Gravell

2
ve belki o zaman kullanın ?? 0 istenilen sonucu almak için?
Christoffer Lette

Kesinlikle işe yarıyor. Denemek için bir birim testi oluşturdum, ancak seçimin sonucunun neden boş bir liste yerine tek bir boş değer olduğunu bulmak için 5 dakika ayırmam gerekecek (sql arka planım kafamı karıştırıyor olabilir ). Bunun için teşekkürler.
Chris Simpson

1
@Lette, eğer değiştirirsem: ondalık sonuç1 = ..... Min () ?? 0; bu da işe yarıyor, bu yüzden girdiniz için teşekkürler.
Chris Simpson

126

İstediğin şey şudur:

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

double result = results.MinOrDefault();

Aslında MinOrDefault()yok. Ancak bunu kendimiz uygulayacak olsaydık, şunun gibi görünürdü:

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

Bununla birlikte, System.Linqaynı sonucu (biraz farklı bir şekilde) üretecek işlevsellik vardır :

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

Eğer resultsdizi öğe içermeyen, DefaultIfEmpty()- bir elementi içeren bir diziyi üretecek default(T)- sonradan çağırabilir Min()üzerinde.

İstediğiniz default(T)şey değilse , şu şekilde kendi varsayılanınızı belirtebilirsiniz:

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

Şimdi, bu harika!


1
@ChristofferLette T'nin sadece boş bir listesini istiyorum, bu yüzden Min () ile Any () kullandım. Teşekkürler!
Adrian Marinică

1
@AdrianMar: BTW, varsayılan olarak bir Boş Nesne kullanmayı düşündünüz mü?
Christoffer Lette

17
Burada bahsedilen MinOrDefault uygulaması, numaralandırılabilir üzerinde iki kez yinelenecektir. Bellek içi koleksiyonlar için önemli değildir, ancak LINQ to Entity veya tembel "getiri dönüşü" yerleşik numaralandırmalar için bu, veritabanına iki gidiş-dönüş veya ilk öğeyi iki kez işleme anlamına gelir. Ben results.DefaultIfEmpty (myDefault) .Min () çözümünü tercih ediyorum.
Kevin Coulombe

4
Kaynağa DefaultIfEmptybakıldığında, gerçekten akıllıca uygulanmıştır, yalnızca yield returns kullanan öğeler varsa diziyi iletir.
Peter Lillevold

2
@JDandChips DefaultIfEmpty, bir IEnumerable<T>. Bir üzerinde adlandırılan varsa IQueryable<T>, bir veritabanı işlemi ile olurdu gibi, o zaman tekil dizisini döndürür, ancak uygun bir oluşturur vermez MethodCallExpression, ve sonuçta sorguya böylece alınacak her şeyi gerektirmez. EnumerableExtensionsBurada önerilen yaklaşımın bu sorunu var.
Jon Hanna

16

Az miktarda bir kodda sadece bir kez yapmak açısından en düzgün olanı, daha önce de belirtildiği gibi:

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

Döküm ile itm.Amountkarşı decimal?ve elde Minnedenle biz bu boş durumu tespit edebilmek istiyorsanız etkileyicisi olmak.

Bununla birlikte, gerçekten bir sağlamak istiyorsanız, MinOrDefault()o zaman elbette şununla başlayabiliriz:

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

Artık MinOrDefaultbir seçici ekleyip eklemediğinize ve varsayılanı belirleyip belirlemediğinize dair tam bir setiniz var .

Bu noktadan itibaren kodunuz basitçe:

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

Yani, başlamak kadar düzgün olmasa da, o andan itibaren daha temiz.

Fakat bekle! Fazlası var!

Diyelim ki EF kullanıyorsunuz ve asyncdestekten yararlanmak istiyorsunuz . Kolayca yapılır:

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

( awaitBurada kullanmadığımı unutmayın ; Task<TSource>ihtiyacımız olanı onsuz yapan ve dolayısıyla awaitgetirdiği gizli komplikasyonlardan kaçınan bir doğrudan yaratabiliriz ).

Ama bekleyin, dahası var! Diyelim ki bunu IEnumerable<T>bazen kullanıyoruz. Yaklaşımımız yetersizdir. Elbette daha iyisini yapabiliriz!

İlk olarak, Mintanımlanmış int?, long?, float? double?ve decimal?zaten (Marc Gravell cevabı markaları kullanımı gibi) biz yine istediğimi yapıyorum. Benzer şekilde, Minbaşka biri için çağrılırsa , önceden tanımlanmış olandan da istediğimiz davranışı elde ederiz T?. Öyleyse, bu gerçeğin avantajlarından yararlanmak için bazı küçük ve dolayısıyla kolayca satır içine alınmış yöntemler yapalım:

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

Şimdi önce daha genel bir durumla başlayalım:

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

Şimdi bundan yararlanan bariz geçersiz kılmalar:

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

Performans konusunda gerçekten iyimser isek, tıpkı Enumerable.Min()yaptığı gibi belirli durumlar için optimizasyon yapabiliriz :

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

Ve böylece için üzerinde long, float, doubleve decimalkümesini maç için Min()sağladığı Enumerable. Bu, T4 şablonlarının yararlı olduğu türden bir şeydir.

Tüm bunların sonunda, MinOrDefault()çok çeşitli türler için umduğumuz kadar performanslı bir uygulamamız var . Kesinlikle tek bir kullanım karşısında "temiz" değil (yine, sadece kullanın DefaultIfEmpty().Min()), ama onu çok kullanırken bulursak çok "temiz", yani yeniden kullanabileceğimiz (veya gerçekten yapıştırabileceğimiz güzel bir kitaplığımız var) StackOverflow'daki yanıtlar…).


0

Bu yaklaşım, içindeki en küçük Amountdeğeri döndürecektir itemList. Teorik olarak bu olmalıdır veritabanına birden yuvarlak geziler kaçının.

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

Null başvuru istisnasına artık neden olmuyor çünkü null atanabilir bir tür kullanıyoruz.

Çağırmadan Anyönce olduğu gibi çalıştırma yöntemlerini kullanmaktan kaçınarak Min, veritabanına yalnızca bir yolculuk yapmalıyız


1
SelectKabul edilen cevapta kullanımının sorguyu birden fazla kez çalıştıracağını düşündüren nedir ? Kabul edilen cevap, tek bir DB çağrısıyla sonuçlanacaktır.
Jon Hanna

Haklısın, Selectertelenmiş bir yöntem ve idama neden olmaz Bu yalanları cevabımdan kaldırdım. Referans: Adam Freeman'dan "Pro ASP.NET MVC4" (Kitap)
JDandChips

İsraf olmadığından emin olmak için gerçekten güçlü olmak istiyorsanız, az önce gönderdiğim cevaba bir göz atın.
Jon Hanna

-1

İtemList null yapılamazsa (burada DefaultIfEmpty 0 verir) ve potansiyel bir çıktı değeri olarak null istiyorsanız, lambda sözdizimini de kullanabilirsiniz:

decimal? result = itemList.Where(x => x.Amount != 0).Min(x => (decimal?)x);
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.