Was ist der beste Weg, um "MinOrDefault" in Linq zu erreichen?

82

Ich erstelle eine Liste von Dezimalwerten aus einem Linq-Ausdruck und möchte den minimalen Wert ungleich Null. Es ist jedoch durchaus möglich, dass der linq-Ausdruck zu einer leeren Liste führt.

Dies löst eine Ausnahme aus und es gibt keinen MinOrDefault, der mit dieser Situation fertig wird.

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

Was ist der beste Weg, um das Ergebnis auf 0 zu setzen, wenn die Liste leer ist?

Chris Simpson
quelle
9
+1 für den Vorschlag, MinOrDefault () zur Bibliothek hinzuzufügen.
J. Andrew Laughlin

Antworten:

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

Beachten Sie die Konvertierung in decimal?. Sie erhalten ein leeres Ergebnis, wenn es keines gibt (behandeln Sie das einfach nachträglich - ich zeige hauptsächlich, wie Sie die Ausnahme stoppen können). Ich habe auch "ungleich Null" verwendet !=anstatt >.

Marc Gravell
quelle
interessant. Ich kann nicht herausfinden, wie dies eine leere Liste vermeiden würde, aber ich werde es versuchen
Chris Simpson
7
Probieren Sie es aus: decimal? result = (new decimal?[0]).Min();gibtnull
Marc Gravell
2
und vielleicht dann benutzen ?? 0, um das gewünschte Ergebnis zu erhalten?
Christoffer Lette
Es funktioniert definitiv. Ich habe gerade einen Komponententest erstellt, um es auszuprobieren, aber ich werde 5 Minuten brauchen, um herauszufinden, warum das Ergebnis der Auswahl ein einzelner Nullwert und keine leere Liste ist (möglicherweise verwirrt mich mein SQL-Hintergrund ). Danke dafür.
Chris Simpson
1
@Lette, wenn ich es ändere in: decimal result1 = ..... Min () ?? 0; Das funktioniert auch, also danke für deine Eingabe.
Chris Simpson
125

Was Sie wollen, ist Folgendes:

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

double result = results.MinOrDefault();

Nun, MinOrDefault()existiert nicht. Aber wenn wir es selbst implementieren würden, würde es ungefähr so ​​aussehen:

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

Es gibt jedoch Funktionen System.Linq, die das gleiche Ergebnis liefern (auf etwas andere Weise):

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

Wenn die resultsSequenz keine Elemente enthält, DefaultIfEmpty()wird eine Sequenz erstellt, die ein Element enthält - das default(T)-, das Sie anschließend aufrufen können Min().

Wenn das default(T)nicht das ist, was Sie wollen, können Sie Ihren eigenen Standard angeben mit:

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

Nun, das ist ordentlich!

Christoffer Lette
quelle
1
@ChristofferLette Ich möchte nur eine leere Liste von T, also habe ich auch Any () mit Min () verwendet. Vielen Dank!
Adrian Marinica
1
@AdrianMar: Übrigens, haben Sie in Betracht gezogen, ein Null-Objekt als Standard zu verwenden?
Christoffer Lette
17
Die hier erwähnte MinOrDefault-Implementierung wird die Aufzählung zweimal durchlaufen. Es spielt keine Rolle für In-Memory-Sammlungen, aber für von LINQ to Entity oder Lazy "Yield Return" erstellte Enumerables bedeutet dies zwei Roundtrips zur Datenbank oder die zweimalige Verarbeitung des ersten Elements. Ich bevorzuge die Ergebnisse. DefaultIfEmpty (myDefault) .Min () Lösung.
Kevin Coulombe
4
Betrachtet man die Quelle nach DefaultIfEmpty, ist sie in der Tat intelligent implementiert und leitet die Sequenz nur weiter, wenn es Elemente gibt, die yield returns verwenden.
Peter Lillevold
2
@JDandChips, die Sie aus der Form zitieren DefaultIfEmpty, nimmt eine IEnumerable<T>. Wenn Sie es auf einem aufgerufen haben IQueryable<T>, wie Sie es bei einer Datenbankoperation getan hätten, gibt es keine Singleton-Sequenz zurück, sondern generiert eine entsprechende MethodCallExpression, sodass für die resultierende Abfrage nicht alles abgerufen werden muss. Der hier vorgeschlagene EnumerableExtensionsAnsatz hat dieses Problem jedoch.
Jon Hanna
16

Das Schönste daran, es nur einmal in einem kleinen Mengencode zu tun, ist, wie bereits erwähnt:

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

Mit Gießen itm.Amountauf decimal?und Erhalt der Mindes Seins , die sauberste , wenn wir diesen leeren Zustand in der Lage sein erkennen wollen.

Wenn Sie jedoch tatsächlich eine bereitstellen möchten, MinOrDefault()können wir natürlich beginnen mit:

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

Sie haben jetzt einen vollständigen Satz darüber, MinOrDefaultob Sie einen Selektor einschließen oder nicht und ob Sie den Standard angeben oder nicht.

Ab diesem Punkt ist Ihr Code einfach:

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

Also, obwohl es anfangs nicht so ordentlich ist, ist es von da an ordentlicher.

Aber warte! Es gibt mehr!

Angenommen, Sie verwenden EF und möchten die asyncUnterstützung nutzen. Einfach gemacht:

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

(Beachten Sie, dass ich awaithier nicht verwende ; wir können direkt ein erstellen Task<TSource>, das das tut, was wir brauchen, ohne es zu verwenden, und somit die versteckten Komplikationen vermeiden await).

Aber warte, da ist noch mehr! Nehmen wir an, wir verwenden dies IEnumerable<T>einige Male. Unser Ansatz ist nicht optimal. Sicher können wir es besser machen!

Erstens, die Mindefiniert auf int?, long?, float? double?und decimal?schon tun , was wir so wie man will (wie Marc GRA Antwort Marken verwenden). In ähnlicher Weise erhalten wir auch das gewünschte Verhalten von dem Minbereits definierten, wenn es für ein anderes aufgerufen wird T?. Lassen Sie uns also einige kleine und daher leicht zu beschreibende Methoden anwenden, um diese Tatsache auszunutzen:

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

Beginnen wir nun mit dem allgemeineren Fall:

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

Nun die offensichtlichen Überschreibungen, die davon Gebrauch machen:

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

Wenn wir in Bezug auf die Leistung wirklich optimistisch sind, können wir für bestimmte Fälle optimieren, genau wie 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();
}

Und so weiter für long, float, doubleund decimaldie Menge des entsprechen , Min()bereitgestellt durch Enumerable. Dies ist die Art von Dingen, bei denen T4-Vorlagen nützlich sind.

Am Ende all dessen haben wir eine so performante Implementierung, MinOrDefault()wie wir es uns erhoffen können, für eine Vielzahl von Typen. Sicherlich nicht "ordentlich" angesichts einer Verwendung dafür (wieder nur verwenden DefaultIfEmpty().Min()), sondern sehr "ordentlich", wenn wir es häufig verwenden, so dass wir eine schöne Bibliothek haben, die wir wiederverwenden (oder tatsächlich einfügen) können Antworten auf StackOverflow…).

Jon Hanna
quelle
0

Dieser Ansatz gibt den kleinsten Einzelwert Amountvon zurück itemList. Theoretisch sollte dies mehrere Roundtrips zur Datenbank vermeiden.

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

Die Nullreferenzausnahme wird nicht mehr verursacht, da wir einen nullbaren Typ verwenden.

Indem Sie die Verwendung von Ausführungsmethoden wie Anyvor dem Aufruf vermeiden Min, sollten wir nur eine Reise in die Datenbank unternehmen

JDandChips
quelle
1
Was lässt Sie denken, dass die Verwendung von Selectin der akzeptierten Antwort die Abfrage mehr als einmal ausführen würde? Die akzeptierte Antwort würde zu einem einzelnen DB-Aufruf führen.
Jon Hanna
Sie haben Recht, Selectist eine verzögerte Methode und würde keine Ausführung verursachen. Ich habe diese Lügen aus meiner Antwort entfernt. Referenz: "Pro ASP.NET MVC4" von Adam Freeman (Buch)
JDandChips
Wenn Sie wirklich optimistisch sein möchten, um sicherzustellen, dass es keine Verschwendung gibt, werfen Sie einen Blick auf die Antwort, die ich gerade gepostet habe.
Jon Hanna
-1

Wenn itemList nicht nullbar ist (wobei DefaultIfEmpty 0 angibt) und Sie null als potenziellen Ausgabewert möchten, können Sie auch die Lambda-Syntax verwenden:

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