519 votes

Comment utiliser LINQ pour sélectionner un objet dont la valeur de la propriété est minimale ou maximale ?

J'ai un objet Personne avec une propriété Nullable DateOfBirth. Existe-t-il un moyen d'utiliser LINQ pour interroger une liste d'objets Personne afin de trouver celui dont la valeur DateOfBirth est la plus ancienne/la plus petite ?

Voici ce avec quoi j'ai commencé :

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

Les valeurs DateOfBirth nulles sont définies par DateTime.MaxValue afin de les exclure de la prise en compte de Min (en supposant qu'au moins une d'entre elles ait une date de naissance spécifiée).

Mais tout ce que cela fait pour moi est de définir firstBornDate à une valeur DateTime. Ce que j'aimerais obtenir, c'est l'objet Personne qui correspond à cette valeur. Dois-je écrire une deuxième requête comme ceci :

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

Ou existe-t-il un moyen plus simple de le faire ?

26 votes

Juste un commentaire sur votre exemple : Vous ne devriez probablement pas utiliser Single ici. Il y aurait une exception si deux personnes avaient la même date de naissance.

1 votes

Voir aussi le quasi-duplicata stackoverflow.com/questions/2736236/ qui contient quelques exemples concis.

4 votes

Quelle fonctionnalité simple et utile. MinBy devrait faire partie de la bibliothèque standard. Nous devrions soumettre une demande de téléchargement à Microsoft github.com/dotnet/corefx

325voto

Paul Betts Points 41354
People.Aggregate((curMin, x) => (curMin == null || (x.DateOfBirth ?? DateTime.MaxValue) <
    curMin.DateOfBirth ? x : curMin))

20 votes

Probablement un peu plus lent que d'implémenter IComparable et d'utiliser Min (ou une boucle for). Mais +1 pour une solution O(n) linqy.

4 votes

De plus, elle doit être < curmin.DateOfBirth . Sinon, vous comparez une DateTime à une Personne.

2 votes

Soyez également prudent lorsque vous utilisez cette fonction pour comparer deux dates. Je l'ai utilisé pour trouver le dernier enregistrement modifié dans une collection non ordonnée. Cela a échoué parce que l'enregistrement que je voulais a fini par avoir la même date et la même heure.

245voto

Jon Skeet Points 692016

Malheureusement, il n'existe pas de méthode intégrée pour ce faire, mais il est assez facile de l'implémenter soi-même. Voici l'essentiel de la méthode :

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

Exemple d'utilisation :

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

Notez que cette opération lèvera une exception si la séquence est vide, et renverra l'option premièrement avec la valeur minimale s'il y en a plus d'un.

Alternativement, vous pouvez utiliser l'implémentation que nous avons dans PlusLINQ en MinBy.cs . (Il y a un correspondant MaxBy bien sûr.)

Installer via la console du gestionnaire de paquets :

PM> Installer le paquetage morelinq

1 votes

Je remplacerais l'Ienumerator + while par un foreach.

5 votes

On ne peut pas le faire facilement à cause du premier appel à MoveNext() avant la boucle. Il existe des alternatives, mais elles sont plus compliquées.

0 votes

Belle méthode. Elle devrait être dans le paquet Linq actuel. Mais pourquoi lancer sur des séquences vides ? La surcharge Min qui retourne réellement un élément de l'énumérable ( msdn.microsoft.com/fr/us/library/bb352408.aspx ) renvoie un résultat nul dans ce cas.

155voto

Lucas Points 10415

REMARQUE : J'ai inclus cette réponse pour être complet car le PO n'a pas mentionné la source des données et nous ne devrions pas faire de suppositions.

Cette requête donne la bonne réponse, mais pourrait être plus lent puisqu'elle pourrait avoir à trier tous les articles dans People en fonction de la structure de données People est :

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

MISE À JOUR : En fait, je ne devrais pas qualifier cette solution de "naïve", mais l'utilisateur doit savoir ce qu'il recherche. La "lenteur" de cette solution dépend des données sous-jacentes. Si c'est un tableau ou List<T> alors LINQ to Objects n'a pas d'autre choix que de trier la collection entière avant de sélectionner le premier élément. Dans ce cas, il sera plus lent que l'autre solution proposée. Cependant, s'il s'agit d'une table LINQ to SQL et que DateOfBirth est une colonne indexée, alors le serveur SQL utilisera l'index au lieu de trier toutes les lignes. Autres fonctions personnalisées IEnumerable<T> Les implémentations pourraient également utiliser des index (voir i4o : LINQ indexé ou la base de données des objets db4o ) et rendent cette solution plus rapide que Aggregate() ou MaxBy() / MinBy() qui ont besoin d'itérer la collection entière une fois. En fait, LINQ to Objects aurait pu (en théorie) faire des cas particuliers en OrderBy() pour les collections triées comme SortedList<T> mais ce n'est pas le cas, pour autant que je sache.

3 votes

Quelqu'un a déjà posté cela, mais l'a apparemment supprimé après que j'ai commenté à quel point c'était lent (et consommateur d'espace) ( vitesse O(n log n) au mieux par rapport à O(n) pour min ). :)

0 votes

Oui, d'où mon avertissement sur le fait qu'il s'agit d'une solution naïve :) cependant, elle est très simple et pourrait être utilisable dans certains cas (petites collections ou si DateOfBirth est une colonne indexée de la base de données).

0 votes

Un autre cas particulier (qui n'existe pas non plus) est qu'il serait possible d'utiliser la connaissance de orderby et first pour faire une recherche de la valeur la plus basse sans trier.

65voto

Rune FS Points 13350
People.OrderBy(p => p.DateOfBirth.GetValueOrDefault(DateTime.MaxValue)).First()

Cela ferait l'affaire

1 votes

Celui-ci est génial ! J'ai utilisé avec OrderByDesending(...).Take(1) dans mon cas de projetion linq.

2 votes

Celui-ci utilise le tri, qui dépasse O(N) temps et utilise également O(N) mémoire.

1 votes

@GeorgePolevoy cela suppose que nous en sachions beaucoup sur la source de données. Si la source de données a déjà un index trié sur le champ donné, alors il s'agirait d'une constante (faible) et ce serait beaucoup plus rapide que la réponse acceptée qui nécessiterait de parcourir la liste entière. Si la source de données est par contre un tableau, vous avez bien sûr raison.

0voto

Matthew Flaschen Points 131723

EDIT encore :

Désolé. En plus de manquer le nullable, je regardais la mauvaise fonction,

Min<(Of <(TSource, TResult>)>)(IEnumerable<(Of <(TSource>)>), Func<(Of <(TSource, TResult>)>)) renvoie bien le type de résultat comme vous l'avez dit.

Je dirais qu'une solution possible est d'implémenter IComparable et d'utiliser Min<(Of <(TSource>)>)(IEnumerable<(Of <(TSource>)>)) qui renvoie réellement un élément de l'IEnumerable. Bien sûr, cela ne vous aide pas si vous ne pouvez pas modifier l'élément. Je trouve la conception de MS un peu bizarre ici.

Bien sûr, vous pouvez toujours faire une boucle for si vous en avez besoin, ou utiliser l'implémentation MoreLINQ donnée par Jon Skeet.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X