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

398

У меня есть объект Person с свойством Nullable DateOfBirth. Есть ли способ использовать LINQ для запроса списка объектов Person для объекта с самым ранним/наименьшим значением DateOfBirth.

Вот что я начал с:

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

Значения Null DateOfBirth устанавливаются в DateTime.MaxValue, чтобы исключить их из рассмотрения Min (при условии, что хотя бы один имеет указанный DOB).

Но все, что для меня нужно, - установить firstBornDate в значение DateTime. То, что я хотел бы получить, - это объект Person, который соответствует этому. Нужно ли писать второй запрос следующим образом:

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

Или существует более простой способ сделать это?

  • 21
    Просто комментарий к вашему примеру: вы, вероятно, не должны использовать Single здесь. Было бы исключение, если бы два человека имели одинаковый DateOfBirth
  • 1
    Смотрите также почти дублированный stackoverflow.com/questions/2736236/… , в котором есть несколько кратких примеров.
Показать ещё 3 комментария
Теги:
linq

13 ответов

254
Лучший ответ
People.Aggregate((curMin, x) => (curMin == null || (x.DateOfBirth ?? DateTime.MaxValue) <
    curMin.DateOfBirth ? x : curMin))
  • 12
    Возможно, немного медленнее, чем просто реализация IComparable и использование Min (или цикла for). Но +1 для решения O (n) linqy.
  • 2
    Кроме того, он должен быть <curmin.DateOfBirth. В противном случае вы сравниваете DateTime с человеком.
Показать ещё 5 комментариев
210

К сожалению, для этого не существует встроенного метода.

PM > Install-Package morelinq

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

В качестве альтернативы вы можете использовать реализацию, которую мы получили в MoreLINQ, в MinBy.cs. (Разумеется, есть соответствующий MaxBy.) Вот некоторые из них:

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

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

  • 1
    Я бы заменил Ienumerator + на foreach
  • 3
    Не может сделать это легко из-за первого вызова MoveNext () перед циклом. Есть альтернативы, но они сложнее ИМО.
Показать ещё 16 комментариев
116

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

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

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

UPDATE: На самом деле я не должен называть это решение "наивным", но пользователю нужно знать, к чему он обращается. Это "медленность" решения зависит от базовых данных. Если это массив или List<T>, то LINQ to Objects не имеет выбора, кроме как сначала сортировать всю коллекцию, прежде чем выбирать первый элемент. В этом случае он будет медленнее, чем предлагалось другим решением. Однако, если это таблица LINQ to 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) в течение минуты). :)
  • 0
    да, отсюда и мое предупреждение о том, что это наивное решение :), однако оно очень простое и в некоторых случаях может быть использовано (небольшие коллекции или если DateOfBirth является индексированным столбцом БД)
Показать ещё 3 комментария
60
People.OrderBy(p => p.DateOfBirth.GetValueOrDefault(DateTime.MaxValue)).First()

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

  • 1
    Это здорово! Я использовал с OrderByDesending (...). Take (1) в моем случае проекции linq.
  • 1
    Этот использует сортировку, которая превышает O (N) время, а также использует O (N) памяти.
Показать ещё 4 комментария
18

Поэтому вы просите ArgMin или ArgMax. У С# нет встроенного API для них.

Я искал чистый и эффективный (O (n) во времени) способ сделать это. И я думаю, что нашел одно:

Общая форма этого шаблона:

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

Специально, используя пример в оригинальном вопросе:

Для С# 7.0 и выше, поддерживающих кортеж value :

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

Для версии С# до 7.0 вместо анонимного типа можно использовать:

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

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

И поскольку оба типа анонимного типа и значения являются типами значений, они должны быть очень эффективными.

НОТА

В моих ArgMin реализациях ArgMin я предположил, что DateOfBirth принимает тип DateTime для простоты и ясности. Исходный вопрос требует исключить те записи с нулевым полем DateOfBirth:

Значения Null DateOfBirth устанавливаются в DateTime.MaxValue, чтобы исключить их из рассмотрения Min (при условии, что хотя бы один имеет определенный DOB).

Это может быть достигнуто с помощью предварительной фильтрации

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

Поэтому это несущественно для вопроса об использовании ArgMin или ArgMax.

ЗАМЕТКА 2

Вышеупомянутый подход имеет оговорку, что, когда есть два экземпляра с одинаковым минимальным значением, реализация Min() будет пытаться сравнить экземпляры в качестве тай-брейкера. Однако, если класс экземпляров не реализует IComparable, тогда будет IComparable ошибка времени выполнения:

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

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

var youngest = Enumerable.Range(0, int.MaxValue)
               .Zip(people, (idx, ppl) => (ppl.DateOfBirth, idx, ppl)).Min().Item3;
  • 1
    Это не работает, когда тип значения является ключом сортировки. «По крайней мере, один объект должен реализовывать IComparable»
  • 0
    слишком большой! это должен быть лучший ответ.
Показать ещё 1 комментарий
6

Решение без дополнительных пакетов:

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, и оно здесь более простое и короткое, чем другие решения.

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

Ниже приводится более общее решение. Он по сути делает то же самое (в порядке O (N)), но и в любых типах IEnumberable и может смешиваться с типами, чьи селектора свойств могут возвращать null.

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

Совершенно простое использование агрегата (эквивалентно сложению на других языках):

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

Единственным недостатком является то, что свойство доступно дважды для каждого элемента последовательности, что может быть дорого. Это трудно исправить.

0

Отметили это только для Entity framework 6.0.0 > : Это можно сделать:

var MaxValue = dbContext.YourDataClass.Select(x => x.ColumnToFindMaxValueFrom).Max();
var MinValue = dbContext.YourDataClass.Select(x => x.ColumnToFindMinValueFrom).Min();
0

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

var firstBorn = People.FirstOrDefault(p => p.DateOfBirth == People.Min(p2 => p2.DateOfBirth));
  • 0
    Разве не намного эффективнее получить минутку перед вашим заявлением linq? var min = People.Min(...); var firstBorn = People.FirstOrDefault(p => p.DateOfBirth == min... В противном случае он получает минимальное количество раз, пока не найдет var min = People.Min(...); var firstBorn = People.FirstOrDefault(p => p.DateOfBirth == min... .
0

ИЗМЕНИТЬ еще раз:

К сожалению. Кроме того, что отсутствует значение nullable, я искал неправильную функцию,

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

Я бы сказал, что одним из возможных решений является реализация IComparable и использование Min < (Of < (TSource > ) > ) (IEnumerable < (Of < (TSource > ) > )), который действительно возвращает элемент из IEnumerable. Конечно, это не поможет вам, если вы не можете изменить элемент. Я считаю, что дизайн MS немного странный.

Конечно, вы всегда можете сделать цикл for, если вам нужно, или использовать реализацию MoreLINQ, которую дал Jon Skeet.

-4

Чтобы получить максимальное или минимальное значение свойства из массива объектов:

Создайте список, в котором хранится каждое значение свойства:

list<int> values = new list<int>;

Добавить все значения свойств в список:

foreach (int i in obj.desiredProperty)
{    values.add(i);  }

Получите максимальное или минимальное количество из списка:

int Max = values.Max;
int Min = values.Min;

Теперь вы можете прокручивать массив объектов и сравнивать значения свойств, которые вы хотите проверить против max или min int:

foreach (obj o in yourArray)
{
    if (o.desiredProperty == Max)
       {return o}

    else if (o.desiredProperty == Min)
        {return o}
}

Ещё вопросы

Сообщество Overcoder
Наверх
Меню