¿Cuál es la mejor manera de lograr "MinOrDefault" en Linq?

82

Estoy produciendo una lista de valores decimales a partir de una expresión linq y quiero el valor mínimo distinto de cero. Sin embargo, es muy posible que la expresión linq resulte en una lista vacía.

Esto generará una excepción y no hay MinOrDefault para hacer frente a esta situación.

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

¿Cuál es la forma más ordenada de establecer el resultado en 0 si la lista está vacía?

Chris Simpson
fuente
9
+1 por sugerir que se agregue MinOrDefault () a la biblioteca.
J. Andrew Laughlin

Respuestas:

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

Tenga en cuenta la conversión a decimal?. Obtendrá un resultado vacío si no hay ninguno (solo maneje eso después del hecho; estoy ilustrando principalmente cómo detener la excepción). También hice uso "distinto de cero" en !=lugar de >.

Marc Gravell
fuente
interesante. No puedo entender cómo esto evitaría una lista vacía, pero lo intentaré
Chris Simpson
7
Pruébelo: decimal? result = (new decimal?[0]).Min();regalanull
Marc Gravell
2
y quizás luego usar ?? 0 para obtener el resultado deseado?
Christoffer Lette
Definitivamente funciona. Acabo de crear una prueba unitaria para probarlo, pero tendré que tomarme 5 minutos para averiguar por qué el resultado de la selección es un valor nulo único en lugar de una lista vacía (es posible que mi fondo de sql me confunda ). Gracias por esto.
Chris Simpson
1
@Lette, si lo cambio a: decimal result1 = ..... Min () ?? 0; esto también funciona, así que gracias por tu aporte.
Chris Simpson
125

Lo que quieres es esto:

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

double result = results.MinOrDefault();

Bueno, MinOrDefault()no existe. Pero si lo implementamos nosotros mismos, se vería así:

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

Sin embargo, hay una funcionalidad System.Linqque producirá el mismo resultado (de una manera ligeramente diferente):

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

Si la resultssecuencia no contiene elementos, DefaultIfEmpty()producirá una secuencia que contenga un elemento, el default(T), al que puede llamar posteriormente Min().

Si default(T)no es lo que desea, puede especificar su propio valor predeterminado con:

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

¡Eso es genial!

Christoffer Lette
fuente
1
@ChristofferLette Solo quiero una lista vacía de T, así que también terminé usando Any () con Min (). ¡Gracias!
Adrian Marinica
1
@AdrianMar: Por cierto, ¿consideró usar un objeto nulo como predeterminado?
Christoffer Lette
17
La implementación de MinOrDefault mencionada aquí iterará sobre el enumerable dos veces. No importa para las colecciones en memoria, pero para LINQ to Entity o enumerables compilados de "rendimiento de rendimiento" perezoso, esto significa dos viajes de ida y vuelta a la base de datos o procesar el primer elemento dos veces. Prefiero la solución results.DefaultIfEmpty (myDefault) .Min ().
Kevin Coulombe
4
En cuanto a la fuente DefaultIfEmpty, de hecho se implementa de manera inteligente, solo se reenvía la secuencia si hay elementos que usan yield returns.
Peter Lillevold
2
@JDandChips está citando de la forma DefaultIfEmptyque toma un IEnumerable<T>. Si lo llamó en un IQueryable<T>, como lo haría con una operación de base de datos, entonces no devuelve una secuencia singleton, sino que genera una apropiada MethodCallExpression, por lo que la consulta resultante no requiere que se recupere todo. Sin EnumerableExtensionsembargo, el enfoque sugerido aquí tiene ese problema.
Jon Hanna
16

Lo mejor en términos de hacerlo una vez en una pequeña cantidad de código es, como ya se mencionó:

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

Con fundición itm.Amounta decimal?y obtener la Minde que siendo el más bonito si queremos ser capaces de detectar esta condición vacía.

Sin embargo, si realmente desea proporcionar un MinOrDefault(), por supuesto, podemos comenzar con:

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

Ahora tiene un conjunto completo de MinOrDefaultsi incluye o no un selector, y si especifica o no el predeterminado.

A partir de este punto, su código es simplemente:

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

Entonces, aunque no es tan ordenado para empezar, está más ordenado a partir de ese momento.

¡Pero espera! ¡Hay más!

Digamos que usa EF y quiere hacer uso del asyncsoporte. Fácil de hacer:

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

(Tenga en cuenta que no lo uso awaitaquí; podemos crear directamente un Task<TSource>que haga lo que necesitamos sin él y, por lo tanto, evitar las complicaciones ocultas que awaittrae).

¡Pero espera hay mas! Digamos que estamos usando esto IEnumerable<T>algunas veces. Nuestro enfoque no es óptimo. ¡Sin duda, podemos hacerlo mejor!

En primer lugar, la Mindefinida en int?, long?, float? double?y decimal?ya hacemos lo que queremos todos modos (como marcas de respuesta de Marc Gravell uso de). Del mismo modo, también obtenemos el comportamiento que queremos del Minya definido si se solicita cualquier otro T?. Así que hagamos algunos métodos pequeños y, por lo tanto, fáciles de integrar para aprovechar este hecho:

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

Ahora comencemos con el caso más general primero:

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

Ahora las anulaciones obvias que hacen uso de esto:

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

Si somos realmente optimistas sobre el rendimiento, podemos optimizar para ciertos casos, al igual que lo Enumerable.Min()hace:

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

Y así sucesivamente para long, float, doubley decimalpara que coincida con el conjunto de Min()proporcionada por Enumerable. Este es el tipo de cosas en las que las plantillas T4 son útiles.

Al final de todo eso, tenemos una implementación casi tan eficaz MinOrDefault()como podríamos esperar, para una amplia gama de tipos. Ciertamente no es "ordenado" a la vista de un uso para él (nuevamente, solo use DefaultIfEmpty().Min()), pero sí mucho "ordenado" si lo usamos mucho, por lo que tenemos una biblioteca agradable que podemos reutilizar (o de hecho, pegar respuestas sobre StackOverflow ...).

Jon Hanna
fuente
0

Este enfoque devolverá el Amountvalor más pequeño de itemList. En teoría, esto debería evitar múltiples viajes de ida y vuelta a la base de datos.

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

La excepción de referencia nula ya no se produce porque estamos usando un tipo que acepta valores NULL.

Al evitar el uso de métodos de ejecución como Anyantes de llamar Min, solo deberíamos hacer un viaje a la base de datos

JDandChips
fuente
1
¿Qué le hace pensar que el uso de Selecten la respuesta aceptada ejecutaría la consulta más de una vez? La respuesta aceptada resultaría en una sola llamada DB.
Jon Hanna
Tienes razón, Selectes un método diferido y no provocaría una ejecución. He eliminado estas mentiras de mi respuesta. Referencia: "Pro ASP.NET MVC4" de Adam Freeman (libro)
JDandChips
Si desea ser realmente optimista para asegurarse de que no haya desperdicio, eche un vistazo a la respuesta que acabo de publicar.
Jon Hanna
-1

Si itemList no admite nulos (donde DefaultIfEmpty da 0) y desea nulo como valor de salida potencial, también puede usar la sintaxis lambda:

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