Tri d'une liste à l'aide d'objets Lambda / Linq

276

J'ai le nom du "tri par propriété" dans une chaîne. Je devrai utiliser Lambda / Linq pour trier la liste des objets.

Ex:

public class Employee
{
  public string FirstName {set; get;}
  public string LastName {set; get;}
  public DateTime DOB {set; get;}
}


public void Sort(ref List<Employee> list, string sortBy, string sortDirection)
{
  //Example data:
  //sortBy = "FirstName"
  //sortDirection = "ASC" or "DESC"

  if (sortBy == "FirstName")
  {
    list = list.OrderBy(x => x.FirstName).toList();    
  }

}
  1. Au lieu d'utiliser un tas d'if pour vérifier le nom de champ (sortBy), existe-t-il une façon plus propre de faire le tri
  2. Le type est-il conscient du type de données?
DotnetDude
la source
3
Dupe: stackoverflow.com/questions/606997/…
Mehrdad Afshari
Je vois sortBy == "FirstName" . L'OP voulait-il plutôt faire .Equals () ?
Pieter
3
@Pieter, il voulait probablement comparer l'égalité, mais je doute qu'il "voulait faire .Equals ()". Les fautes de frappe ne donnent généralement pas lieu à un code qui fonctionne.
C.Evenhuis
1
@Pieter Votre question n'a de sens que si vous pensez qu'il y a un problème avec ==... quoi?
Jim Balter

Réponses:

367

Cela peut être fait comme

list.Sort( (emp1,emp2)=>emp1.FirstName.CompareTo(emp2.FirstName) );

Le framework .NET transforme le lambda (emp1,emp2)=>inten unComparer<Employee>.

Cela a l'avantage d'être fortement typé.

gls123
la source
Il m'est souvent arrivé d'écrire des opérateurs de comparaison complexes, impliquant finalement plusieurs critères de comparaison et une comparaison GUID à sécurité intégrée pour garantir l'antisymétrie. Souhaitez-vous utiliser une expression lambda pour une comparaison complexe comme celle-ci? Sinon, cela signifie-t-il que les comparaisons d'expression lambda ne devraient être limitées qu'à des cas simples?
Simone
4
ouais je ne le vois pas non plus quelque chose comme ça? list.Sort ((emp1, emp2) => emp1.GetType (). GetProperty (sortBy) .GetValue (emp1, null) .CompareTo (emp2.GetType (). GetProperty (sortBy) .GetValue (emp2, null)))) ;
Sam
1
comment trier en sens inverse?
JerryGoyal
1
@JerryGoyal échanger les paramètres ... emp2.FirstName.CompareTo (emp1.FirstName) etc.
Chris Hynes
3
Ce n'est pas parce qu'il s'agit d'une référence de fonction qu'il doit s'agir d'une seule ligne. Vous pouvez simplement écrirelist.sort(functionDeclaredElsewhere)
The Hoff
74

Une chose que vous pourriez faire est de changer Sortafin de mieux utiliser les lambdas.

public enum SortDirection { Ascending, Descending }
public void Sort<TKey>(ref List<Employee> list,
                       Func<Employee, TKey> sorter, SortDirection direction)
{
  if (direction == SortDirection.Ascending)
    list = list.OrderBy(sorter);
  else
    list = list.OrderByDescending(sorter);
}

Vous pouvez maintenant spécifier le champ à trier lors de l'appel de la Sortméthode.

Sort(ref employees, e => e.DOB, SortDirection.Descending);
Samuel
la source
7
Étant donné que la colonne de tri est dans une chaîne, vous aurez toujours besoin d'un bloc switch / if-else pour déterminer la fonction à transmettre.
tvanfosson
1
Vous ne pouvez pas faire cette supposition. Qui sait comment son code l'appelle.
Samuel
3
Il a déclaré dans la question que le "tri par propriété" est dans une chaîne. Je vais juste par sa question.
tvanfosson
6
Je pense que c'est plus probable car cela provient d'un contrôle de tri sur une page Web qui transmet la colonne de tri en tant que paramètre de chaîne. Ce serait mon cas d'utilisation, de toute façon.
tvanfosson
2
@tvanfosson - Vous avez raison, j'ai un contrôle personnalisé qui a l'ordre et le nom du champ sous forme de chaîne
DotnetDude
55

Vous pouvez utiliser Reflection pour obtenir la valeur de la propriété.

list = list.OrderBy( x => TypeHelper.GetPropertyValue( x, sortBy ) )
           .ToList();

Où TypeHelper a une méthode statique comme:

public static class TypeHelper
{
    public static object GetPropertyValue( object obj, string name )
    {
        return obj == null ? null : obj.GetType()
                                       .GetProperty( name )
                                       .GetValue( obj, null );
    }
}

Vous pouvez également consulter Dynamic LINQ à partir de la bibliothèque d'échantillons VS2008 . Vous pouvez utiliser l'extension IEnumerable pour convertir la liste en un IQueryable, puis utiliser l'extension Dynamic Link OrderBy.

 list = list.AsQueryable().OrderBy( sortBy + " " + sortDirection );
tvanfosson
la source
1
Bien que cela résout son problème, nous pourrions vouloir l'empêcher d'utiliser une chaîne pour le trier. Bonne réponse néanmoins.
Samuel
Vous pouvez utiliser Dynamic linq sans Linq to Sql pour faire ce dont il a besoin ... J'adore
JoshBerke
Sûr. Vous pouvez le convertir en IQueryable. Je n'y ai pas pensé. Mise à jour de ma réponse.
tvanfosson
@Samuel Si le tri arrive en tant que variable de route, il n'y a pas d'autre moyen de le trier.
Chev
1
@ChuckD - mettez la collection en mémoire avant d'essayer de l'utiliser, par exemplecollection.ToList().OrderBy(x => TypeHelper.GetPropertyValue( x, sortBy)).ToList();
tvanfosson
20

Voici comment j'ai résolu mon problème:

List<User> list = GetAllUsers();  //Private Method

if (!sortAscending)
{
    list = list
           .OrderBy(r => r.GetType().GetProperty(sortBy).GetValue(r,null))
           .ToList();
}
else
{
    list = list
           .OrderByDescending(r => r.GetType().GetProperty(sortBy).GetValue(r,null))
           .ToList();
}
Cornel Urian
la source
16

Construire l'ordre par expression peut être lu ici

Sans vergogne volé de la page en lien:

// First we define the parameter that we are going to use
// in our OrderBy clause. This is the same as "(person =>"
// in the example above.
var param = Expression.Parameter(typeof(Person), "person");

// Now we'll make our lambda function that returns the
// "DateOfBirth" property by it's name.
var mySortExpression = Expression.Lambda<Func<Person, object>>(Expression.Property(param, "DateOfBirth"), param);

// Now I can sort my people list.
Person[] sortedPeople = people.OrderBy(mySortExpression).ToArray();
Rashack
la source
Il y a des problèmes associés à ceci: Tri DateTime.
CrazyEnigma
Et qu'en est-il des classes composites, c'est-à-dire Person.Employer.CompanyName?
davewilliams459
Je faisais essentiellement la même chose et cette réponse l'a résolu.
Jason.Net
8

Vous pouvez utiliser la réflexion pour accéder à la propriété.

public List<Employee> Sort(List<Employee> list, String sortBy, String sortDirection)
{
   PropertyInfo property = list.GetType().GetGenericArguments()[0].
                                GetType().GetProperty(sortBy);

   if (sortDirection == "ASC")
   {
      return list.OrderBy(e => property.GetValue(e, null));
   }
   if (sortDirection == "DESC")
   {
      return list.OrderByDescending(e => property.GetValue(e, null));
   }
   else
   {
      throw new ArgumentOutOfRangeException();
   }
}

Remarques

  1. Pourquoi passez-vous la liste par référence?
  2. Vous devez utiliser une énumération pour le sens du tri.
  3. Vous pourriez obtenir une solution beaucoup plus propre si vous passiez une expression lambda spécifiant la propriété à trier au lieu du nom de la propriété sous forme de chaîne.
  4. Dans mon exemple de liste == null provoquera une NullReferenceException, vous devez intercepter ce cas.
Daniel Brückner
la source
Quelqu'un d'autre a-t-il déjà remarqué qu'il s'agit d'un type de retour vide mais renvoie des listes?
emd
Au moins, personne ne s'est soucié de le réparer et je ne l'ai pas remarqué car je n'ai pas écrit le code à l'aide d'un IDE. Merci d'avoir fait remarquer cela.
Daniel Brückner
6

Trier utilise l'interface IComparable, si le type l'implémente. Et vous pouvez éviter les ifs en implémentant un IComparer personnalisé:

class EmpComp : IComparer<Employee>
{
    string fieldName;
    public EmpComp(string fieldName)
    {
        this.fieldName = fieldName;
    }

    public int Compare(Employee x, Employee y)
    {
        // compare x.fieldName and y.fieldName
    }
}

puis

list.Sort(new EmpComp(sortBy));
Serguei
la source
FYI: Sort est une méthode de List <T> et n'est pas une extension Linq.
Serguei
5

Réponse pour 1:

Vous devriez pouvoir créer manuellement une arborescence d'expressions qui peut être passée dans OrderBy en utilisant le nom comme chaîne. Ou vous pouvez utiliser la réflexion comme suggéré dans une autre réponse, ce qui pourrait être moins de travail.

Modifier : Voici un exemple pratique de construction manuelle d'un arbre d'expression. (Tri sur X.Value, quand on ne connaît que le nom "Value" de la propriété). Vous pourriez (devriez) construire une méthode générique pour le faire.

using System;
using System.Linq;
using System.Linq.Expressions;

class Program
{
    private static readonly Random rand = new Random();
    static void Main(string[] args)
    {
        var randX = from n in Enumerable.Range(0, 100)
                    select new X { Value = rand.Next(1000) };

        ParameterExpression pe = Expression.Parameter(typeof(X), "value");
        var expression = Expression.Property(pe, "Value");
        var exp = Expression.Lambda<Func<X, int>>(expression, pe).Compile();

        foreach (var n in randX.OrderBy(exp))
            Console.WriteLine(n.Value);
    }

    public class X
    {
        public int Value { get; set; }
    }
}

Cependant, pour construire un arbre d'expression, vous devez connaître les types participants. Cela pourrait ou non être un problème dans votre scénario d'utilisation. Si vous ne savez pas sur quel type vous devez trier, il sera probablement plus facile d'utiliser la réflexion.

Réponse pour 2 .:

Oui, puisque Comparer <T> .Par défaut sera utilisé pour la comparaison, si vous ne définissez pas explicitement le comparateur.

driis
la source
Avez-vous un exemple de construction d'une arborescence d'expressions à passer dans OrderBy?
DotnetDude
4
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Linq.Expressions;

public static class EnumerableHelper
{

    static MethodInfo orderBy = typeof(Enumerable).GetMethods(BindingFlags.Static | BindingFlags.Public).Where(x => x.Name == "OrderBy" && x.GetParameters().Length == 2).First();

    public static IEnumerable<TSource> OrderBy<TSource>(this IEnumerable<TSource> source, string propertyName)
    {
        var pi = typeof(TSource).GetProperty(propertyName, BindingFlags.Public | BindingFlags.FlattenHierarchy | BindingFlags.Instance);
        var selectorParam = Expression.Parameter(typeof(TSource), "keySelector");
        var sourceParam = Expression.Parameter(typeof(IEnumerable<TSource>), "source");
        return 
            Expression.Lambda<Func<IEnumerable<TSource>, IOrderedEnumerable<TSource>>>
            (
                Expression.Call
                (
                    orderBy.MakeGenericMethod(typeof(TSource), pi.PropertyType), 
                    sourceParam, 
                    Expression.Lambda
                    (
                        typeof(Func<,>).MakeGenericType(typeof(TSource), pi.PropertyType), 
                        Expression.Property(selectorParam, pi), 
                        selectorParam
                    )
                ), 
                sourceParam
            )
            .Compile()(source);
    }

    public static IEnumerable<TSource> OrderBy<TSource>(this IEnumerable<TSource> source, string propertyName, bool ascending)
    {
        return ascending ? source.OrderBy(propertyName) : source.OrderBy(propertyName).Reverse();
    }

}

Un autre, cette fois pour tout IQueryable:

using System;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;

public static class IQueryableHelper
{

    static MethodInfo orderBy = typeof(Queryable).GetMethods(BindingFlags.Static | BindingFlags.Public).Where(x => x.Name == "OrderBy" && x.GetParameters().Length == 2).First();
    static MethodInfo orderByDescending = typeof(Queryable).GetMethods(BindingFlags.Static | BindingFlags.Public).Where(x => x.Name == "OrderByDescending" && x.GetParameters().Length == 2).First();

    public static IQueryable<TSource> OrderBy<TSource>(this IQueryable<TSource> source, params string[] sortDescriptors)
    {
        return sortDescriptors.Length > 0 ? source.OrderBy(sortDescriptors, 0) : source;
    }

    static IQueryable<TSource> OrderBy<TSource>(this IQueryable<TSource> source, string[] sortDescriptors, int index)
    {
        if (index < sortDescriptors.Length - 1) source = source.OrderBy(sortDescriptors, index + 1);
        string[] splitted = sortDescriptors[index].Split(' ');
        var pi = typeof(TSource).GetProperty(splitted[0], BindingFlags.Public | BindingFlags.FlattenHierarchy | BindingFlags.Instance | BindingFlags.IgnoreCase);
        var selectorParam = Expression.Parameter(typeof(TSource), "keySelector");
        return source.Provider.CreateQuery<TSource>(Expression.Call((splitted.Length > 1 && string.Compare(splitted[1], "desc", StringComparison.Ordinal) == 0 ? orderByDescending : orderBy).MakeGenericMethod(typeof(TSource), pi.PropertyType), source.Expression, Expression.Lambda(typeof(Func<,>).MakeGenericType(typeof(TSource), pi.PropertyType), Expression.Property(selectorParam, pi), selectorParam)));
    }

}

Vous pouvez passer plusieurs critères de tri, comme ceci:

var q = dc.Felhasznalos.OrderBy(new string[] { "Email", "FelhasznaloID desc" });
Andras Vass
la source
4

La solution fournie par Rashack ne fonctionne malheureusement pas pour les types de valeur (int, enums, etc.).

Pour qu'il fonctionne avec n'importe quel type de propriété, voici la solution que j'ai trouvée:

public static Expression<Func<T, object>> GetLambdaExpressionFor<T>(this string sortColumn)
    {
        var type = typeof(T);
        var parameterExpression = Expression.Parameter(type, "x");
        var body = Expression.PropertyOrField(parameterExpression, sortColumn);
        var convertedBody = Expression.MakeUnary(ExpressionType.Convert, body, typeof(object));

        var expression = Expression.Lambda<Func<T, object>>(convertedBody, new[] { parameterExpression });

        return expression;
    }
Antoine Jaussoin
la source
C'est génial et même correctement traduit en SQL!
Xavier Poinas
1

Ajoutant à ce que @Samuel et @bluish ont fait. C'est beaucoup plus court car l'Enum n'était pas nécessaire dans ce cas. De plus, comme bonus supplémentaire lorsque l'Ascendant est le résultat souhaité, vous ne pouvez passer que 2 paramètres au lieu de 3 car true est la réponse par défaut au troisième paramètre.

public void Sort<TKey>(ref List<Person> list, Func<Person, TKey> sorter, bool isAscending = true)
{
    list = isAscending ? list.OrderBy(sorter) : list.OrderByDescending(sorter);
}
Stephen Whitlock
la source
0

Si vous obtenez le nom et la direction de tri de la colonne sous forme de chaîne et que vous ne souhaitez pas utiliser la syntaxe switch ou if \ else pour déterminer la colonne, cet exemple peut vous intéresser:

private readonly Dictionary<string, Expression<Func<IuInternetUsers, object>>> _sortColumns = 
        new Dictionary<string, Expression<Func<IuInternetUsers, object>>>()
    {
        { nameof(ContactSearchItem.Id),             c => c.Id },
        { nameof(ContactSearchItem.FirstName),      c => c.FirstName },
        { nameof(ContactSearchItem.LastName),       c => c.LastName },
        { nameof(ContactSearchItem.Organization),   c => c.Company.Company },
        { nameof(ContactSearchItem.CustomerCode),   c => c.Company.Code },
        { nameof(ContactSearchItem.Country),        c => c.CountryNavigation.Code },
        { nameof(ContactSearchItem.City),           c => c.City },
        { nameof(ContactSearchItem.ModifiedDate),   c => c.ModifiedDate },
    };

    private IQueryable<IuInternetUsers> SetUpSort(IQueryable<IuInternetUsers> contacts, string sort, string sortDir)
    {
        if (string.IsNullOrEmpty(sort))
        {
            sort = nameof(ContactSearchItem.Id);
        }

        _sortColumns.TryGetValue(sort, out var sortColumn);
        if (sortColumn == null)
        {
            sortColumn = c => c.Id;
        }

        if (string.IsNullOrEmpty(sortDir) || sortDir == SortDirections.AscendingSort)
        {
            contacts = contacts.OrderBy(sortColumn);
        }
        else
        {
            contacts = contacts.OrderByDescending(sortColumn);
        }

        return contacts;
    }

Solution basée sur l'utilisation de Dictionary qui connecte les colonnes de tri nécessaires via Expression> et sa chaîne de clé.

En ligne123321
la source