Как использовать LINQ для выбора объекта с минимальным или максимальным значением свойства


466

У меня есть объект 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 здесь. Было бы исключение, если бы два человека имели одинаковую дату рождения
Ники,

1
См. Также почти дублированный stackoverflow.com/questions/2736236/… , в котором есть несколько кратких примеров.
Goodeye

4
Какая простая и полезная функция. MinBy должен быть в стандартной библиотеке. Мы должны отправить запрос на извлечение в Microsoft github.com/dotnet/corefx
полковник Panic

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 является самой длинной длиной любого слова, но не сказать вам , что самое длинное слово есть .
полковник Паник

Ответы:


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

16
Возможно, немного медленнее, чем просто реализация IComparable и использование Min (или цикла for). Но +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 + на foreach
ggf31416

5
Не может сделать это легко из-за первого вызова MoveNext () перед циклом. Есть альтернативы, но они сложнее ИМО.
Джон Скит

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

8
Я удостоил ответа Пола из-за небиблиотечного решения, но спасибо за этот код и ссылку на библиотеку MoreLINQ, которую, я думаю, я начну использовать!
slolife


135

ПРИМЕЧАНИЕ. Я включил этот ответ для полноты, поскольку в ОП не упоминается, что является источником данных, и мы не должны делать никаких предположений.

Этот запрос дает правильный ответ, но может быть медленнее, поскольку может потребоваться отсортировать все элементы в Peopleзависимости от структуры данных People:

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

ОБНОВЛЕНИЕ: На самом деле я не должен называть это решение «наивным», но пользователь должен знать, к чему он обращается. «Медлительность» этого решения зависит от базовых данных. Если это массив или List<T>, то у LINQ to Objects нет другого выбора, кроме как сначала отсортировать всю коллекцию перед выбором первого элемента. В этом случае это будет медленнее, чем предложенное другое решение. Однако, если это таблица LINQ to SQL и DateOfBirthиндексированный столбец, SQL Server будет использовать индекс вместо сортировки всех строк. Другие пользовательские IEnumerable<T>реализации могут также использовать индексы (см. I4o: Indexed LINQ или объектную базу данных db4o ) и сделать это решение быстрее, чемAggregate() или MaxBy()/MinBy()которые нужно перебрать всю коллекцию один раз. На самом деле, LINQ to Objects (теоретически) мог бы создать особые случаи OrderBy()для отсортированных коллекций, как SortedList<T>, но, насколько я знаю, это не так.


1
Кто-то уже опубликовал это, но, по-видимому, удалил его после того, как я прокомментировал, насколько медленным (и занимающим много места) это было (скорость O (n log n) в лучшем случае по сравнению с O (n) за мин). :)
Мэтью Флэшен

да, отсюда и моё предупреждение о том, что это наивное решение :), однако оно очень простое и может быть использовано в некоторых случаях (небольшие коллекции или если DateOfBirth является столбцом индексированной БД)
Lucas

Другой особый случай (которого там тоже нет) заключается в том, что можно было бы использовать знания orderby и сначала выполнить поиск по наименьшему значению без сортировки.
Rune FS

Сортировка коллекции - это операция Nlog (N), которая не лучше линейной или O (n) временной сложности. Если нам просто нужен 1 элемент / объект из последовательности, которая является минимальной или максимальной, я думаю, что мы должны придерживаться линейной временной сложности.
Явар Муртаза

@yawar коллекция может быть уже отсортирована (более вероятно проиндексирована), и в этом случае вы можете иметь O (log n)
Rune FS

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

Сделал бы трюк


1
Это здорово! Я использовал с OrderByDesending (...). Take (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, будет выдана ошибка времени выполнения:

По крайней мере один объект должен реализовывать IComparable

К счастью, это все еще можно исправить довольно чисто. Идея состоит в том, чтобы связать отдаленный «идентификатор» с каждой записью, которая служит однозначным нарушителем связей. Мы можем использовать инкрементный идентификатор для каждой записи. Все еще используя возраст людей в качестве примера:

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

1
Это не работает, когда тип значения является ключом сортировки. «По крайней мере, один объект должен реализовывать IComparable»
Лян

1
слишком большой! это должен быть лучший ответ.
Гвидо Мокко

@лян, да, хороший улов. К счастью, есть еще чистое решение для этого. Смотрите обновленное решение в разделе «Примечание 2».
KFL

Выбор может дать вам идентификатор! var youngest = people.Select ((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

Другая реализация, которая может работать с обнуляемыми клавишами выбора и для коллекции ссылочного типа, возвращает нуль, если подходящих элементов не найдено. Это может быть полезно, например, для обработки результатов базы данных.

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

0

Попробуйте следующую идею:

var firstBornDate = People.GroupBy(p => p.DateOfBirth).Min(g => g.Key).FirstOrDefault();

-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...В противном случае он получает мин несколько раз, пока не найдет тот, который вы ищете.
Ниеминен,
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.