Який найоптимальніший спосіб досягти “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 хвилин, щоб зрозуміти, чому результатом select є одне нульове значення, а не порожній список (можливо, мій фон sql мене бентежить ). Дякую за це.
Кріс Сімпсон,

1
@Lette, якщо я зміню його на: десятковий результат1 = ..... Хв () ?? 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: BTW, чи не розглядали Ви як за замовчуванням використання Null Object ?
Christoffer Lette

17
Згадана тут реалізація MinOrDefault повторюватиме перелічені двічі. Це не має значення для колекцій в пам’яті, але для LINQ to Entity або для ледачих вбудованих перечислюваних елементів «return return» це означає два кругові поїздки до бази даних або обробку першого елемента двічі. Я віддаю перевагу результатам. Рішення DefaultEfEmpty (myDefault) .Min ().
Kevin Coulombe

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

Виняток нульового посилання більше не спричиняється, оскільки ми використовуємо тип, що має нульовий статус.

Уникаючи використання таких методів, як Anyперед викликом Min, ми повинні здійснити лише одну поїздку до бази даних


1
Що змушує вас думати, що використання Selectу прийнятій відповіді може виконати запит не один раз? Прийнята відповідь призведе до одного виклику БД.
Джон Ханна,

Ви маєте рацію, Selectце відкладений метод і не призведе до виконання. Я видалив ці брехні зі своєї відповіді. Довідка: "Pro ASP.NET MVC4" Адама Фрімена (книга)
JDandChips

Якщо ви хочете бути по-справжньому бичачим, переконуючись, що немає марнотратства, погляньте на відповідь, яку я щойно розмістив.
Джон Ханна,

-1

Якщо itemList не має нульового значення (де DefaultIfEmpty дає 0), і ви бажаєте значення null як потенційне вихідне значення, ви також можете використовувати синтаксис лямбда:

decimal? result = itemList.Where(x => x.Amount != 0).Min(x => (decimal?)x);
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.