web-dev-qa-db-fra.com

Comment effectuer .Max () sur une propriété de tous les objets d'une collection et renvoyer l'objet avec la valeur maximale

J'ai une liste d'objets qui ont deux propriétés int. La liste est la sortie d'une autre requête linq. L'object:

public class DimensionPair  
{
    public int Height { get; set; }
    public int Width { get; set; }
}

Je veux trouver et retourner l'objet dans la liste qui a la plus grande valeur de propriété Height.

Je peux réussir à obtenir la valeur la plus élevée de la valeur Height mais pas l'objet lui-même.

Puis-je faire cela avec Linq? Comment?

257
theringostarrs

Nous avons un méthode d'extension pour faire exactement cela dans MoreLINQ . Vous pouvez regarder l’implémentation ici, mais fondamentalement, il s’agit de parcourir les données, en se rappelant l’élément maximum que nous avons vu jusqu’à présent et la valeur maximale qu’elle a produite sous la projection.

Dans votre cas, vous feriez quelque chose comme:

var item = items.MaxBy(x => x.Height);

Ceci est meilleur (IMO) que l’une des solutions présentées ici, autre que la deuxième solution de Mehrdad (qui est fondamentalement la même chose que MaxBy):

  • C'est O(n) contrairement à réponse précédente acceptée qui trouve la valeur maximale à chaque itération (la rendant ainsi O (n ^ 2))
  • La solution de commande est O (n log n)
  • Prendre la valeur Max puis trouver le premier élément avec cette valeur est O (n), mais itère deux fois sur la séquence. Dans la mesure du possible, vous devriez utiliser LINQ en mode passe unique.
  • C'est beaucoup plus simple à lire et à comprendre que la version agrégée, et n'évalue la projection qu'une fois par élément
260
Jon Skeet

Cela nécessiterait un tri (O (n log n)) mais est très simple et flexible. Un autre avantage est de pouvoir l'utiliser avec LINQ to SQL:

var maxObject = list.OrderByDescending(item => item.Height).First();

Notez que cela a l’avantage d’énumérer la séquence list une seule fois. Bien que cela puisse avoir peu d'importance si list est un List<T> qui ne change pas entre-temps, cela pourrait être important pour les objets IEnumerable<T> arbitraires. Rien ne garantit que la séquence ne change pas dans différentes énumérations, aussi les méthodes qui le font plusieurs fois peuvent être dangereuses (et inefficaces, en fonction de la nature de la séquence). Cependant, cela reste une solution moins qu'idéale pour les grandes séquences. Je suggère d’écrire manuellement votre propre extension MaxObject si vous disposez d’un grand nombre d’articles pour pouvoir le faire en un seul passage, sans trier ni effectuer d’autres tâches (O (n)):

static class EnumerableExtensions {
    public static T MaxObject<T,U>(this IEnumerable<T> source, Func<T,U> selector)
      where U : IComparable<U> {
       if (source == null) throw new ArgumentNullException("source");
       bool first = true;
       T maxObj = default(T);
       U maxKey = default(U);
       foreach (var item in source) {
           if (first) {
                maxObj = item;
                maxKey = selector(maxObj);
                first = false;
           } else {
                U currentKey = selector(item);
                if (currentKey.CompareTo(maxKey) > 0) {
                    maxKey = currentKey;
                    maxObj = item;
                }
           }
       }
       if (first) throw new InvalidOperationException("Sequence is empty.");
       return maxObj;
    }
}

et l'utiliser avec:

var maxObject = list.MaxObject(item => item.Height);
187
Mehrdad Afshari

Effectuer une commande puis sélectionner le premier article vous fait perdre beaucoup de temps à les commander après le premier. Vous ne vous souciez pas de l'ordre de ceux-ci.

À la place, vous pouvez utiliser la fonction d'agrégation pour sélectionner le meilleur élément en fonction de vos besoins.

var maxHeight = dimensions
    .Aggregate((agg, next) => 
        next.Height > agg.Height ? next : agg);

var maxHeightAndWidth = dimensions
    .Aggregate((agg, next) => 
        next.Height >= agg.Height && next.Width >= agg.Width ? next: agg);
133
Cameron MacFarland

Et pourquoi n'essayes-tu pas avec ça ??? :

var itemsMax = items.Where(x => x.Height == items.Max(y => y.Height));

OU plus optimiser:

var itemMaxHeight = items.Max(y => y.Height);
var itemsMax = items.Where(x => x.Height == itemMaxHeight);

mmm?

32
Dragouf

Les réponses à ce jour sont super! Mais je vois un besoin de solution avec les contraintes suivantes:

  1. LINQ simple et concis;
  2. O (n) complexité;
  3. Ne pas évaluer la propriété plus d'une fois par élément.

C'est ici:

public static T MaxBy<T, R>(this IEnumerable<T> en, Func<T, R> evaluate) where R : IComparable<R> {
    return en.Select(t => new Tuple<T, R>(t, evaluate(t)))
        .Aggregate((max, next) => next.Item2.CompareTo(max.Item2) > 0 ? next : max).Item1;
}

public static T MinBy<T, R>(this IEnumerable<T> en, Func<T, R> evaluate) where R : IComparable<R> {
    return en.Select(t => new Tuple<T, R>(t, evaluate(t)))
        .Aggregate((max, next) => next.Item2.CompareTo(max.Item2) < 0 ? next : max).Item1;
}

Usage:

IEnumerable<Tuple<string, int>> list = new[] {
    new Tuple<string, int>("other", 2),
    new Tuple<string, int>("max", 4),
    new Tuple<string, int>("min", 1),
    new Tuple<string, int>("other", 3),
};
Tuple<string, int> min = list.MinBy(x => x.Item2); // "min", 1
Tuple<string, int> max = list.MaxBy(x => x.Item2); // "max", 4
20
meustrus

Je pense que le tri en fonction de la colonne dont vous souhaitez obtenir le MAX et la saisie de la première devraient fonctionner. Cependant, s'il y a plusieurs objets avec la même valeur MAX, un seul sera saisi:

private void Test()
{
    test v1 = new test();
    v1.Id = 12;

    test v2 = new test();
    v2.Id = 12;

    test v3 = new test();
    v3.Id = 12;

    List<test> arr = new List<test>();
    arr.Add(v1);
    arr.Add(v2);
    arr.Add(v3);

    test max = arr.OrderByDescending(t => t.Id).First();
}

class test
{
    public int Id { get; set; }
}
3
regex

Dans NHibernate (avec NHibernate.Linq), vous pouvez le faire comme suit:

return session.Query<T>()
              .Single(a => a.Filter == filter &&
                           a.Id == session.Query<T>()
                                          .Where(a2 => a2.Filter == filter)
                                          .Max(a2 => a2.Id));

Ce qui va générer du SQL comme suit:

select *
from TableName foo
where foo.Filter = 'Filter On String'
and foo.Id = (select cast(max(bar.RowVersion) as INT)
              from TableName bar
              where bar.Name = 'Filter On String')

Ce qui me semble plutôt efficace.

2
Tod Thomson

Vous pouvez également mettre à niveau la solution de Mehrdad Afshari en réécrivant la méthode d'extention en une méthode plus rapide (et plus esthétique):

static class EnumerableExtensions
{
    public static T MaxElement<T, R>(this IEnumerable<T> container, Func<T, R> valuingFoo) where R : IComparable
    {
        var enumerator = container.GetEnumerator();
        if (!enumerator.MoveNext())
            throw new ArgumentException("Container is empty!");

        var maxElem = enumerator.Current;
        var maxVal = valuingFoo(maxElem);

        while (enumerator.MoveNext())
        {
            var currVal = valuingFoo(enumerator.Current);

            if (currVal.CompareTo(maxVal) > 0)
            {
                maxVal = currVal;
                maxElem = enumerator.Current;
            }
        }

        return maxElem;
    }
}

Et puis juste l'utiliser:

var maxObject = list.MaxElement(item => item.Height);

Ce nom sera clair pour les personnes utilisant C++ (car std :: max_element est présent).

1
Maciej Oziębły

D'après la réponse initiale de Cameron, voici ce que je viens d'ajouter à ma version améliorée de FloatingWindowHost de la bibliothèque SilverFlow (copie de FloatingWindowHost.cs à l'adresse http://clipflair.codeplex.com code source)

    public int MaxZIndex
    {
      get {
        return FloatingWindows.Aggregate(-1, (maxZIndex, window) => {
          int w = Canvas.GetZIndex(window);
          return (w > maxZIndex) ? w : maxZIndex;
        });
      }
    }

    private void SetTopmost(UIElement element)
    {
        if (element == null)
            throw new ArgumentNullException("element");

        Canvas.SetZIndex(element, MaxZIndex + 1);
    }

Il est à noter que, dans le code ci-dessus, Canvas.ZIndex est une propriété attachée disponible pour les droits UIE dans divers conteneurs, et non uniquement utilisée lorsqu'elle est hébergée dans un canevas (voir Contrôle de l'ordre de rendu (ZOrder) dans Silverlight sans utiliser le contrôle Canvas ). On pourrait même faire une méthode d’extension statique SetTopmost et SetBottomMost pour UIElement facilement en adaptant ce code.

1
George Birbilis