Як використовувати LINQ для вибору об'єкта з мінімальним або максимальним значенням властивості


465

У мене є об'єкт Person із властивістю Nullable DateOfBirth. Чи можна використовувати LINQ для запиту списку об'єктів Person для того, хто має найдавніше / найменше значення DateOfBirth.

Ось з чого я почав:

var firstBornDate = People.Min(p => p.DateOfBirth.GetValueOrDefault(DateTime.MaxValue));

Нульові значення DateOfBirth встановлюються на DateTime.MaxValue, щоб виключити їх з мінімального розгляду (якщо принаймні одне має вказаний DOB).

Але для мене все, що потрібно зробити, це встановити firstBornDate на значення DateTime. Що я хотів би отримати, це об'єкт Person, який відповідає цьому. Чи потрібно мені написати другий запит так:

var firstBorn = People.Single(p=> (p.DateOfBirth ?? DateTime.MaxValue) == firstBornDate);

Або існує менший спосіб зробити це?


24
Просто коментар до вашого прикладу: Ви, мабуть, не повинні використовувати Single тут. Виняток було б, якби двоє людей мали однаковий DateOfBirth
Нікі

1
Дивіться також майже повторюваний stackoverflow.com/questions/2736236/… , який має кілька стислих прикладів.
goodeye

4
Яка проста і корисна особливість. MinBy повинен бути в стандартній бібліотеці. Ми повинні подати запит на тягнення
Полковник Паніка

2
Це, здається, існує сьогодні, просто надайте функцію вибору властивості:a.Min(x => x.foo);
jackmott

4
Щоб продемонструвати проблему: в Python max("find a word of maximal length in this sentence".split(), key=len)повертає рядок 'речення'. У C # "find a word of maximal length in this sentence".Split().Max(word => word.Length)вираховує , що 8 є найдовшою довжиною будь-якого слова, але не сказати вам , що найдовше слово є .
Полковник Паніка

Відповіді:


298
People.Aggregate((curMin, x) => (curMin == null || (x.DateOfBirth ?? DateTime.MaxValue) <
    curMin.DateOfBirth ? x : curMin))

16
Можливо, трохи повільніше, ніж просто реалізувати IComparable та використовувати Min (або для циклу). Але +1 для рішення O (n) linqy.
Меттью Флашен

3
Крім того, він повинен бути <curmin.DateOfBirth. В іншому випадку ви порівнюєте DateTime з особою.
Метью Флашен

2
Також будьте обережні, використовуючи це, щоб порівняти дві дати. Я використовував це для пошуку останнього запису змін у невпорядкованій колекції. Не вдалося, оскільки запис, який я хотів, закінчився однаковою датою та часом.
Саймон Гілл

8
Чому ти робиш зайву перевірку curMin == null? curMinможе бути лише в тому nullвипадку, якщо ви використовували Aggregate()з насінням, яке є null.
Гарної ночі Nerd Pride


226

На жаль, не існує вбудованого методу для цього, але це досить просто реалізувати для себе. Ось кишки цього:

public static TSource MinBy<TSource, TKey>(this IEnumerable<TSource> source,
    Func<TSource, TKey> selector)
{
    return source.MinBy(selector, null);
}

public static TSource MinBy<TSource, TKey>(this IEnumerable<TSource> source,
    Func<TSource, TKey> selector, IComparer<TKey> comparer)
{
    if (source == null) throw new ArgumentNullException("source");
    if (selector == null) throw new ArgumentNullException("selector");
    comparer = comparer ?? Comparer<TKey>.Default;

    using (var sourceIterator = source.GetEnumerator())
    {
        if (!sourceIterator.MoveNext())
        {
            throw new InvalidOperationException("Sequence contains no elements");
        }
        var min = sourceIterator.Current;
        var minKey = selector(min);
        while (sourceIterator.MoveNext())
        {
            var candidate = sourceIterator.Current;
            var candidateProjected = selector(candidate);
            if (comparer.Compare(candidateProjected, minKey) < 0)
            {
                min = candidate;
                minKey = candidateProjected;
            }
        }
        return min;
    }
}

Приклад використання:

var firstBorn = People.MinBy(p => p.DateOfBirth ?? DateTime.MaxValue);

Зауважте, що це викине виняток, якщо послідовність порожня, і поверне перший елемент з мінімальним значенням, якщо їх більше ніж один.

Крім того, ви можете використовувати реалізацію, яку ми отримали в MoreLINQ , в MinBy.cs . (Звичайно, є відповідна MaxBy.)

Встановити через консоль менеджера пакунків:

PM> Install-Package morelinq


1
Я б замінив Ienumerator + в той час як передбачення
ggf31416

5
Це не легко зробити через перший виклик MoveNext () перед циклом. Є альтернативи, але вони суттєвіші IMO.
Джон Скіт

2
Хоча я міг повернути дефолт (T), який вважає мене невідповідним. Це більше відповідає таким методам, як First () та підходу індексатора словника. Ви можете легко адаптувати його, якщо хочете.
Джон Скіт

8
Відповідь я присудив Павлу через небібліотечне рішення, але дякую за цей код і посилання на бібліотеку MoreLINQ, яку, думаю, почну використовувати!
slolife


135

ПРИМІТКА: Я включаю цю відповідь для повноти, оскільки ОП не згадувало, що таке джерело даних, і ми не повинні робити жодних припущень.

Цей запит дає правильну відповідь, але може бути повільніше, оскільки, можливо, доведеться сортувати всі елементи People, залежно від структури даних People:

var oldest = People.OrderBy(p => p.DateOfBirth ?? DateTime.MaxValue).First();

ОНОВЛЕННЯ: Насправді я не повинен називати це рішення "наївним", але користувачеві потрібно знати, проти чого він запитує. "Повільність" цього рішення залежить від основних даних. Якщо це масив або List<T>, то LINQ для об'єктів не має іншого вибору, крім того, щоб спочатку сортувати всю колекцію перед вибором першого елемента. У цьому випадку це буде повільніше, ніж запропоновано інше рішення. Однак якщо це таблиця LINQ до SQL і DateOfBirthє індексованим стовпцем, то SQL Server використовуватиме індекс замість сортування всіх рядків. Інші спеціалізовані IEnumerable<T>реалізації також можуть використовувати індекси (див. I4o: індексований LINQ або об’єктну базу даних db4o ) та робити це рішення швидше, ніж Aggregate()або MaxBy()/MinBy()які потрібно повторити всю колекцію один раз. Насправді, LINQ to Objects (теоретично) міг би робити спеціальні випадки OrderBy()для сортованих колекцій на кшталт SortedList<T>, але це не так, наскільки я знаю.


1
Хтось це вже розмістив, але, мабуть, видалив його після того, як я прокоментував, наскільки повільною (і просторовою) була швидкість (O (n log n) швидкість у кращому випадку порівняно з O (n) протягом хв. :)
Метью Флашен

так, отже, моє попередження про наївне рішення :) однак воно просто мертве і може бути корисним у деяких випадках (невеликі колекції або якщо DateOfBirth є індексованим стовпцем БД)
Лукас,

Інший особливий випадок (якого теж немає) полягає в тому, що можна було б скористатися знаннями orderby і спочатку здійснити пошук найнижчого значення без сортування.
Руна ФС

Сортування колекції - це операція Nlog (N), яка не краща за лінійну чи O (n) часову складність. Якщо нам просто потрібен 1 елемент / об’єкт із послідовності, яка є min або max, я думаю, що ми повинні дотримуватися лінійної часової компромісності.
Явар Муртаза

@yawar колекція може вже бути відсортованою (індексованою ймовірніше); у цьому випадку ви можете мати O (log n)
Rune FS

63
People.OrderBy(p => p.DateOfBirth.GetValueOrDefault(DateTime.MaxValue)).First()

Зробив би трюк


1
Цей чудовий! Я використовував з OrderByDesending (...). Візьміть (1) у моєму випадку прогнозу linq.
Ведран Мандич

1
У цьому використовується сортування, яке перевищує O (N) час, а також використовує O (N) пам'ять.
Георгій Полевой

@GeorgePolevoy, який передбачає, що ми знаємо досить багато про джерело даних. Якщо джерело даних вже має відсортований індекс у заданому полі, то це було б (низькою) константою, і це було б набагато швидше, ніж прийнята відповідь, яка потребує проходження всього списку. Якщо з іншого боку джерелом даних є, наприклад, масив, ви, звичайно, праві
Rune FS

@RuneFS - все ж це слід згадати у своїй відповіді, оскільки це важливо.
rory.ap

Вистава перетягне вас. Я навчився цього важким шляхом. Якщо ви хочете об'єкт зі значенням Min або Max, то вам не потрібно сортувати весь масив. Всього достатньо одного сканування. Подивіться на прийняту відповідь або подивіться пакет MoreLinq.
Sau001

35

Таким чином , ви просите ArgMinабо ArgMax. C # не має вбудованого API для них.

Я шукав чистий та ефективний (O (n) в часі) спосіб це зробити. І я думаю, що знайшов одне:

Загальною формою цієї схеми є:

var min = data.Select(x => (key(x), x)).Min().Item2;
                            ^           ^       ^
              the sorting key           |       take the associated original item
                                Min by key(.)

Спеціально, використовуючи приклад у оригінальному запитанні:

Для C # 7.0 і вище, що підтримує кортеж значення :

var youngest = people.Select(p => (p.DateOfBirth, p)).Min().Item2;

Для версії C # до 7.0 замість цього можна використовувати анонімний тип :

var youngest = people.Select(p => new { ppl = p; age = p.DateOfBirth }).Min().ppl;

Вони працюють , тому що обидва значення кортежу і анонімний тип мають осмислені компараторов по замовчуванням: для (x1, y1) і (x2, y2), вона спочатку порівнює x1проти x2, то y1проти y2. Ось чому вбудований .Minможе використовуватися для цих типів.

А оскільки анонімний тип і кортеж значення є типовими типами, вони повинні бути дуже ефективними.

ПРИМІТКА

У своїх вищезгаданих ArgMinреалізаціях я передбачав DateOfBirthприйняти тип DateTimeдля простоти та ясності. Оригінальне запитання вимагає виключити ці записи з нульовим DateOfBirthполем:

Нульові значення DateOfBirth встановлюються на DateTime.MaxValue, щоб виключити їх з мінімального розгляду (якщо принаймні одне має вказаний DOB).

Це можна досягти за допомогою попередньої фільтрації

people.Where(p => p.DateOfBirth.HasValue)

Тож це не має значення щодо питання про реалізацію ArgMinчи ArgMax.

ПРИМІТКА 2

Наведений вище підхід має застереження, що коли є два екземпляри, які мають однакове мінімальне значення, тоді Min()реалізація намагатиметься порівняти екземпляри як вимикач. Однак якщо клас екземплярів не реалізується IComparable, буде викинута помилка виконання:

Принаймні один об’єкт повинен реалізовувати Ізрівнянний

На щастя, це все ще можна виправити досить чисто. Ідея полягає в тому, щоб пов'язати дистанційну "ідентифікацію" з кожним записом, який служить однозначним вимикачем. Ми можемо використовувати додатковий ідентифікатор для кожного запису. Досі користуються віком людей як приклад:

var youngest = Enumerable.Range(0, int.MaxValue)
               .Zip(people, (idx, ppl) => (ppl.DateOfBirth, idx, ppl)).Min().Item3;

1
Схоже, це не працює, коли тип сортування є ключем сортування. "Принаймні один об'єкт повинен реалізовувати IComparable"
Лян

1
занадто здорово! це має бути найкращою відповіддю.
Гвідо Моча

@liang так гарний улов. На щастя, ще є чисте рішення для цього. Дивіться оновлене рішення у розділі "Примітка 2".
KFL

Вибір може дати вам посвідчення особи! вар молодший = люди.Виберіть ((p, i) => (p.DateOfBirth, i, p)). Min (). Item2;
Джеремі

19

Рішення без додаткових пакетів:

var min = lst.OrderBy(i => i.StartDate).FirstOrDefault();
var max = lst.OrderBy(i => i.StartDate).LastOrDefault();

також ви можете загорнути його в розширення:

public static class LinqExtensions
{
    public static T MinBy<T, TProp>(this IEnumerable<T> source, Func<T, TProp> propSelector)
    {
        return source.OrderBy(propSelector).FirstOrDefault();
    }

    public static T MaxBy<T, TProp>(this IEnumerable<T> source, Func<T, TProp> propSelector)
    {
        return source.OrderBy(propSelector).LastOrDefault();
    }
}

і в цьому випадку:

var min = lst.MinBy(i => i.StartDate);
var max = lst.MaxBy(i => i.StartDate);

До речі ... O (n ^ 2) - не найкраще рішення. Пол Беттс дав жирніше рішення, ніж мій. Але моє все-таки рішення LINQ, і воно тут простіше і коротше, ніж інші рішення.


3
public class Foo {
    public int bar;
    public int stuff;
};

void Main()
{
    List<Foo> fooList = new List<Foo>(){
    new Foo(){bar=1,stuff=2},
    new Foo(){bar=3,stuff=4},
    new Foo(){bar=2,stuff=3}};

    Foo result = fooList.Aggregate((u,v) => u.bar < v.bar ? u: v);
    result.Dump();
}

3

Ідеально просте використання агрегату (еквівалентно складенню іншими мовами):

var firstBorn = People.Aggregate((min, x) => x.DateOfBirth < min.DateOfBirth ? x : min);

Єдиним недоліком є ​​те, що доступ до властивості здійснюється двічі за елемент послідовності, що може бути дорого. Це важко виправити.


1

Далі йде більш загальне рішення. Це по суті робить те саме (у порядку O (N)), але для будь-яких типів IEnumberable і може змішуватися з типами, вибрані властивості можуть повернути нуль.

public static class LinqExtensions
{
    public static T MinBy<T>(this IEnumerable<T> source, Func<T, IComparable> selector)
    {
        if (source == null)
        {
            throw new ArgumentNullException(nameof(source));
        }
        if (selector == null)
        {
            throw new ArgumentNullException(nameof(selector));
        }
        return source.Aggregate((min, cur) =>
        {
            if (min == null)
            {
                return cur;
            }
            var minComparer = selector(min);
            if (minComparer == null)
            {
                return cur;
            }
            var curComparer = selector(cur);
            if (curComparer == null)
            {
                return min;
            }
            return minComparer.CompareTo(curComparer) > 0 ? cur : min;
        });
    }
}

Тести:

var nullableInts = new int?[] {5, null, 1, 4, 0, 3, null, 1};
Assert.AreEqual(0, nullableInts.MinBy(i => i));//should pass

0

ЗНО ЗНО:

Вибачте. Окрім пропуску нульового, я дивився на неправильну функцію,

Min <(Of <(TSource, TResult>)>) (IEnumerable <(Of <(TSource>)>), Func <(Of <(TSource, TResult>)>)) повертає тип результату, як ви сказали.

Я б сказав, що одне можливе рішення - реалізувати IComparable і використовувати Min <(Of <(TSource>)>) (IEnumerable <(Of <(TSource>)>)) , що дійсно повертає елемент з IEnumerable. Звичайно, це не допоможе вам, якщо ви не можете змінити елемент. Я вважаю, що дизайн MS тут трохи дивний.

Звичайно, ви завжди можете зробити цикл for, якщо вам потрібно, або скористатися реалізацією MoreLINQ, яку надав Джон Скіт.


0

Інша реалізація, яка могла б працювати з нульовими селекторними ключами, і для колекції еталонного типу повертає null, якщо не знайдено відповідних елементів. Це може бути корисним, наприклад, при обробці результатів бази даних.

  public static class IEnumerableExtensions
  {
    /// <summary>
    /// Returns the element with the maximum value of a selector function.
    /// </summary>
    /// <typeparam name="TSource">The type of the elements of source.</typeparam>
    /// <typeparam name="TKey">The type of the key returned by keySelector.</typeparam>
    /// <param name="source">An IEnumerable collection values to determine the element with the maximum value of.</param>
    /// <param name="keySelector">A function to extract the key for each element.</param>
    /// <exception cref="System.ArgumentNullException">source or keySelector is null.</exception>
    /// <exception cref="System.InvalidOperationException">source contains no elements.</exception>
    /// <returns>The element in source with the maximum value of a selector function.</returns>
    public static TSource MaxBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector) => MaxOrMinBy(source, keySelector, 1);

    /// <summary>
    /// Returns the element with the minimum value of a selector function.
    /// </summary>
    /// <typeparam name="TSource">The type of the elements of source.</typeparam>
    /// <typeparam name="TKey">The type of the key returned by keySelector.</typeparam>
    /// <param name="source">An IEnumerable collection values to determine the element with the minimum value of.</param>
    /// <param name="keySelector">A function to extract the key for each element.</param>
    /// <exception cref="System.ArgumentNullException">source or keySelector is null.</exception>
    /// <exception cref="System.InvalidOperationException">source contains no elements.</exception>
    /// <returns>The element in source with the minimum value of a selector function.</returns>
    public static TSource MinBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector) => MaxOrMinBy(source, keySelector, -1);


    private static TSource MaxOrMinBy<TSource, TKey>
      (IEnumerable<TSource> source, Func<TSource, TKey> keySelector, int sign)
    {
      if (source == null) throw new ArgumentNullException(nameof(source));
      if (keySelector == null) throw new ArgumentNullException(nameof(keySelector));
      Comparer<TKey> comparer = Comparer<TKey>.Default;
      TKey value = default(TKey);
      TSource result = default(TSource);

      bool hasValue = false;

      foreach (TSource element in source)
      {
        TKey x = keySelector(element);
        if (x != null)
        {
          if (!hasValue)
          {
            value = x;
            result = element;
            hasValue = true;
          }
          else if (sign * comparer.Compare(x, value) > 0)
          {
            value = x;
            result = element;
          }
        }
      }

      if ((result != null) && !hasValue)
        throw new InvalidOperationException("The source sequence is empty");

      return result;
    }
  }

Приклад:

public class A
{
  public int? a;
  public A(int? a) { this.a = a; }
}

var b = a.MinBy(x => x.a);
var c = a.MaxBy(x => x.a);

-2

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

var firstBorn = People.FirstOrDefault(p => p.DateOfBirth == People.Min(p2 => p2.DateOfBirth));

Чи не буде набагато ефективнішим отримати хв перед вашою заявою linq? var min = People.Min(...); var firstBorn = People.FirstOrDefault(p => p.DateOfBirth == min...Інакше він отримує хвилину повторно, поки не знайде того, кого шукаєш.
Nieminen
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.