Jaki jest najlepszy sposób na osiągnięcie „MinOrDefault” w Linq?

82

Tworzę listę wartości dziesiętnych z wyrażenia linq i chcę minimalną wartość niezerową. Jednak jest całkowicie możliwe, że wyrażenie linq spowoduje powstanie pustej listy.

Spowoduje to wyjątek i nie ma MinOrDefault, który poradziłby sobie z tą sytuacją.

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

Jaki jest najładniejszy sposób ustawienia wyniku na 0, jeśli lista jest pusta?

Chris Simpson
źródło
9
+1 za zasugerowanie dodania MinOrDefault () do biblioteki.
J. Andrew Laughlin

Odpowiedzi:

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

Zwróć uwagę na konwersję do decimal?. Otrzymasz pusty wynik, jeśli go nie ma (po prostu zajmij się tym po fakcie - głównie ilustruję, jak zatrzymać wyjątek). Użyłem też !=raczej wartości „niezerowej” niż >.

Marc Gravell
źródło
ciekawy. Nie potrafię wymyślić, jak to pozwoliłoby uniknąć pustej listy, ale spróbuję
Chris Simpson
7
Spróbuj: decimal? result = (new decimal?[0]).Min();dajenull
Marc Gravell
2
a może wtedy użyj ?? 0, aby uzyskać pożądany efekt?
Christoffer Lette
To zdecydowanie działa. Właśnie zbudowałem test jednostkowy, aby go wypróbować, ale będę musiał poświęcić 5 minut na ustalenie, dlaczego wynikiem selekcji jest pojedyncza wartość null, a nie pusta lista (możliwe, że moje tło sql mnie myli ). Dzięki za to.
Chris Simpson,
1
@Lette, jeśli zmienię to na: decimal result1 = ..... Min () ?? 0; to też działa, więc dziękuję za Twój wkład.
Chris Simpson,
125

Chcesz to:

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

double result = results.MinOrDefault();

Cóż, MinOrDefault()nie istnieje. Ale gdybyśmy sami to zaimplementowali, wyglądałoby to mniej więcej tak:

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

Istnieje jednak funkcjonalność, System.Linqktóra da ten sam wynik (w nieco inny sposób):

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

Jeśli resultssekwencja nie zawiera żadnych elementów, DefaultIfEmpty()utworzy sekwencję zawierającą jeden element - the default(T)- do którego możesz następnie przywołać Min().

Jeśli default(T)nie jest tym, czego chcesz, możesz określić własne ustawienie domyślne za pomocą:

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

Teraz jest fajnie!

Christoffer Lette
źródło
1
@ChristofferLette Chcę tylko pustej listy T, więc skończyło się również na użyciu Any () z Min (). Dzięki!
Adrian Marinica
1
@AdrianMar: BTW, czy rozważałeś użycie obiektu zerowego jako domyślnego?
Christoffer Lette
17
Wspomniana tutaj implementacja MinOrDefault będzie iterować przez wyliczalne dwa razy. Nie ma to znaczenia w przypadku kolekcji w pamięci, ale w przypadku LINQ to Entity lub leniwych wbudowanych elementów wyliczeniowych typu „yield return” oznacza to dwie rundy do bazy danych lub dwukrotne przetwarzanie pierwszego elementu. Wolę wyniki.DefaultIfEmpty (myDefault) .Min ().
Kevin Coulombe,
4
Patrząc na źródło DefaultIfEmpty, jest on rzeczywiście zaimplementowany inteligentnie, przekazuje sekwencję tylko wtedy, gdy istnieją elementy używające yield returns.
Peter Lillevold
2
@JDandChips cytujesz w formie, DefaultIfEmptyktóra przyjmuje rozszerzenie IEnumerable<T>. Jeśli wywołałeś go w operacji IQueryable<T>, tak jak w przypadku operacji na bazie danych, to nie zwraca pojedynczej sekwencji, ale generuje odpowiednią MethodCallExpression, więc wynikowe zapytanie nie wymaga pobrania wszystkiego. Sugerowane EnumerableExtensionstutaj podejście ma jednak ten problem.
Jon Hanna
16

Jak już wspomniano, najładniejszy pod względem zrobienia tego raz w małej ilości kodu jest:

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

Z odlewania itm.Amountdo decimal?i uzyskiwania Minz tego bycia neatest jeśli chcemy być w stanie wykryć tę pustą warunek.

Jeśli jednak chcesz faktycznie zapewnić MinOrDefault(), możemy oczywiście zacząć od:

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

Masz teraz pełny zestaw tego, MinOrDefaultczy chcesz dołączyć selektor i czy określasz wartość domyślną, czy nie.

Od tego momentu Twój kod jest po prostu:

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

Tak więc, chociaż na początku nie jest tak schludnie, odtąd jest schludniej.

Ale poczekaj! Jest więcej!

Powiedzmy, że używasz EF i chcesz skorzystać ze asyncwsparcia. Łatwe do zrobienia:

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

(Zwróć uwagę, że nie używam awaittutaj; możemy bezpośrednio stworzyć Task<TSource>bez niego to, czego potrzebujemy, a tym samym uniknąć ukrytych komplikacji await).

Ale czekaj, jest więcej! Powiedzmy, że używamy tego IEnumerable<T>czasami. Nasze podejście jest nieoptymalne. Z pewnością możemy zrobić to lepiej!

Po pierwsze, Minokreśla się na int?, long?, float? double?a decimal?już w każdym razie to, co my chcemy (jako odpowiedź marki Marc Gravell korzystają z). Podobnie, otrzymujemy również zachowanie, które chcemy, z Minjuż zdefiniowanego, jeśli zostanie wywołane do innego T?. Zróbmy więc kilka małych, a przez to łatwych do wprowadzenia metod, aby skorzystać z tego faktu:

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

Teraz zacznijmy od bardziej ogólnego przypadku:

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

Teraz oczywiste nadpisania, które wykorzystują to:

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

Jeśli naprawdę zależy nam na wydajności, możemy zoptymalizować ją pod kątem określonych przypadków, tak jak 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();
}

I tak dalej long, float, doublei decimaldopasować zestaw Min()zapewnia Enumerable. W takich sytuacjach przydatne są szablony T4.

Na koniec mamy tak wydajną implementację, MinOrDefault()na jaką mogliśmy liczyć, dla szerokiej gamy typów. Z pewnością nie „schludny” w obliczu jednego użycia do tego (ponownie, po prostu użyj DefaultIfEmpty().Min()), ale bardzo „schludny”, jeśli często go używamy, więc mamy fajną bibliotekę, którą możemy ponownie wykorzystać (lub rzeczywiście wkleić do odpowiedzi na StackOverflow…).

Jon Hanna
źródło
0

To podejście zwróci pojedynczą najmniejszą Amountwartość z itemList. Teoretycznie powinno to zapobiec wielokrotnym podróżom w obie strony do bazy danych.

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

Wyjątek odwołania o wartości null nie jest już powodowany, ponieważ używamy typu dopuszczającego wartość null.

Unikając wykonywania metod, takich jak Anyprzed wywołaniem Min, powinniśmy odbyć tylko jedną podróż do bazy danych

JDandChips
źródło
1
Dlaczego sądzisz, że użycie Selectw zaakceptowanej odpowiedzi spowodowałoby wykonanie zapytania więcej niż jeden raz? Zaakceptowana odpowiedź spowodowałaby pojedyncze wywołanie DB.
Jon Hanna
Masz rację, Selectjest to metoda odroczona i nie spowodowałaby wykonania. Usunąłem te kłamstwa z mojej odpowiedzi. Źródła: „Pro ASP.NET MVC4” autorstwa Adama Freemana (książka)
JDandChips,
Jeśli chcesz być naprawdę uparty, upewniając się, że nie ma odpadów, spójrz na odpowiedź, którą właśnie opublikowałem.
Jon Hanna
-1

Jeśli itemList nie dopuszcza wartości null (gdzie DefaultIfEmpty daje 0) i chcesz mieć wartość null jako potencjalną wartość wyjściową, możesz również użyć składni lambda:

decimal? result = itemList.Where(x => x.Amount != 0).Min(x => (decimal?)x);
Jason
źródło