Какой самый простой способ достичь «MinOrDefault» в Linq?

82

Я создаю список десятичных значений из выражения linq, и мне нужно минимальное ненулевое значение. Однако вполне возможно, что выражение linq приведет к пустому списку.

Это вызовет исключение, и нет MinOrDefault, чтобы справиться с этой ситуацией.

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

Какой самый простой способ установить результат на 0, если список пуст?

Крис Симпсон
источник
9
+1 за предложение добавить MinOrDefault () в библиотеку.
Дж. Эндрю Лафлин

Ответы:

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

Обратите внимание на преобразование в decimal?. Вы получите пустой результат, если его нет (просто обработайте это постфактум - я в основном показываю, как остановить исключение). Я также сделал "ненулевое" использование, !=а не >.

Марк Гравелл
источник
интересно. Я не могу понять, как это избежать пустого списка, но я попробую
Крис Симпсон
7
Попробуй: decimal? result = (new decimal?[0]).Min();даетnull
Марк Гравелл
2
а разве тогда использовать ?? 0 получить желаемый результат?
Christoffer Lette,
Это определенно работает. Я только что создал модульный тест, чтобы опробовать его, но мне нужно потратить 5 минут на то, чтобы выяснить, почему результатом выбора является одно нулевое значение, а не пустой список (возможно, мой фон sql сбивает меня с толку ). Спасибо за это.
Крис Симпсон
1
@Lette, если я изменю его на: decimal result1 = ..... Min () ?? 0; это тоже работает, поэтому спасибо за ваш вклад.
Крис Симпсон
126

Что вы хотите:

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

double result = results.MinOrDefault();

Ну MinOrDefault()не существует. Но если бы мы реализовали это сами, это выглядело бы примерно так:

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

Однако есть функция, System.Linqкоторая даст такой же результат (немного другим способом):

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

Если resultsпоследовательность не содержит элементов, DefaultIfEmpty()будет создана последовательность, содержащая один элемент default(T)- который вы впоследствии можете вызвать Min().

Если default(T)это не то, что вы хотите, вы можете указать собственное значение по умолчанию с помощью:

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

Вот это здорово!

Кристоффер Летте
источник
1
@ChristofferLette Мне нужен только пустой список T, поэтому я также использовал Any () с Min (). Благодаря!
Адриан Мариника
1
@AdrianMar: Кстати, вы рассматривали вариант использования Null Object по умолчанию?
Christoffer Lette
17
Упомянутая здесь реализация MinOrDefault будет перебирать перечисляемый дважды. Это не имеет значения для коллекций в памяти, но для LINQ to Entity или ленивых построенных перечислимых элементов yield return это означает два обращения к базе данных или обработку первого элемента дважды. Я предпочитаю решение results.DefaultIfEmpty (myDefault) .Min ().
Кевин Куломб
4
Глядя на источник DefaultIfEmpty, он действительно реализован умно, только пересылка последовательности выполняется, если есть элементы, использующие yield returns.
Питер Лиллевольд
2
@JDandChips, которую вы цитируете из формы, DefaultIfEmptyкоторая принимает IEnumerable<T>. Если вы вызвали его IQueryable<T>, например, для операции с базой данных, тогда он не возвращает одноэлементную последовательность, но генерирует соответствующую MethodCallExpression, и поэтому результирующий запрос не требует, чтобы все было извлечено. Однако предлагаемый EnumerableExtensionsздесь подход имеет эту проблему.
Джон Ханна
16

Самым изящным с точки зрения того, чтобы сделать это один раз в небольшом количестве кода, является, как уже упоминалось:

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

С приведением itm.Amountк типу decimal?и получением Minтого, что является самым аккуратным, если мы хотим иметь возможность обнаруживать это пустое состояние.

Однако, если вы действительно хотите предоставить, MinOrDefault()мы, конечно, можем начать с:

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

Теперь у вас есть полный набор того, MinOrDefaultвключаете ли вы селектор и указываете ли вы значение по умолчанию.

С этого момента ваш код просто:

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

Итак, хотя сначала это не так аккуратно, с тех пор стало еще аккуратнее.

Но ждать! Есть еще кое-что!

Допустим, вы используете EF и хотите воспользоваться asyncподдержкой. Легко сделать:

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

(Обратите внимание, что я awaitздесь не использую ; мы можем напрямую создать без него Task<TSource>то, что нам нужно, и, следовательно, избежать скрытых осложнений await).

Но подождите, это еще не все! Допустим, мы используем это IEnumerable<T>несколько раз. Наш подход неоптимален. Конечно, мы можем добиться большего!

Во- первых, Minопределенная на int?, long?, float? double?и decimal?уже делать то , что мы хотим , чтобы в любом случае (как ответ марки Marc Gravell по использованию). Точно так же мы также получаем желаемое поведение от Minуже определенного, если оно вызывается для любого другого T?. Итак, давайте сделаем несколько небольших и, следовательно, легко встроенных методов, чтобы воспользоваться этим фактом:

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

Теперь давайте начнем сначала с более общего случая:

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

Теперь очевидные переопределения, которые используют это:

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

Если мы действительно оптимистичны в отношении производительности, мы можем оптимизировать для определенных случаев, например 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();
}

И так далее для long, float, doubleи decimalв соответствии с набором Min()обеспечиваетсяEnumerable . В таких случаях полезны шаблоны T4.

В конце концов, у нас есть почти такая же производительная реализация, на которую MinOrDefault()мы могли надеяться, для широкого диапазона типов. Конечно, не "аккуратно" перед лицом одного использования (опять же, просто использовать DefaultIfEmpty().Min()), но очень "аккуратно", если мы обнаружим, что используем его много, поэтому у нас есть хорошая библиотека, которую мы можем повторно использовать (или, действительно, вставить в ответы на StackOverflow…).

Джон Ханна
источник
0

Этот подход вернет единственное наименьшее Amountзначение из itemList. Теоретически это должно избежать многократных обращений к базе данных.

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

Исключение нулевой ссылки больше не вызывается, потому что мы используем тип, допускающий значение NULL.

Избегая использования методов выполнения, таких как Anyперед вызовом Min, мы должны совершить только одно посещение базы данных.

JDandChips
источник
1
Что заставляет вас думать, что использование Selectв принятом ответе приведет к выполнению запроса более одного раза? Принятый ответ приведет к одному вызову БД.
Джон Ханна
Вы правы, Selectэто отложенный метод и не вызовет выполнения. Я удалил эту ложь из своего ответа. Ссылка: «Pro ASP.NET MVC4» от Адама Фримана (книга)
JDandChips
Если вы хотите быть по-настоящему оптимистичными и убедиться, что нет потерь, взгляните на только что опубликованный мной ответ.
Джон Ханна
-1

Если itemList не допускает значения NULL (где DefaultIfEmpty дает 0) и вы хотите, чтобы NULL в качестве потенциального выходного значения, вы также можете использовать синтаксис лямбда:

decimal? result = itemList.Where(x => x.Amount != 0).Min(x => (decimal?)x);
Джейсон
источник