Classificando uma lista usando Lambda / Linq para objetos

276

Eu tenho o nome do "classificar por propriedade" em uma seqüência de caracteres. Vou precisar usar o Lambda / Linq para classificar a lista de objetos.

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. Em vez de usar um monte de ifs para verificar o nome do campo (sortBy), existe uma maneira mais limpa de fazer a classificação
  2. A classificação está ciente do tipo de dados?
DotnetDude
fonte
3
Dupe: stackoverflow.com/questions/606997/...
Mehrdad Afshari
Eu vejo sortBy == "FirstName" . O OP quis fazer .Equals () ?
Pieter
3
@ Pieter ele provavelmente quis comparar a igualdade, mas duvido que ele "pretendesse fazer .Equals ()". Erros de digitação geralmente não resultam em código que funcione.
C.Evenhuis
1
@ Pieter Sua pergunta só faz sentido se você acha que há algo errado com ==... o quê?
Jim Balter

Respostas:

367

Isso pode ser feito como

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

O framework .NET está lançando o lambda (emp1,emp2)=>intcomo umComparer<Employee>.

Isso tem a vantagem de ser fortemente digitado.

gls123
fonte
Muitas vezes acontecia escrever operadores de comparação complexos, envolvendo vários critérios de comparação e uma comparação GUID à prova de falhas no final, para garantir antisimetria. Você usaria uma expressão lambda para uma comparação complexa como essa? Caso contrário, isso significa que as comparações da expressão lambda devem ser limitadas apenas a casos simples?
Simone
4
Sim, eu não vejo algo assim? Você pode usar o seguinte método: ();) ();) ();) ();) ();) ();) ();) (); ;
Sat
1
como ordenar ao contrário?
JerryGoyal
1
@JerryGoyal trocar os parâmetros ... emp2.FirstName.CompareTo (emp1.FirstName) etc.
Chris Hynes
3
Só porque é uma referência de função, não precisa ser um liner. Você poderia escreverlist.sort(functionDeclaredElsewhere)
The Hoff
74

Uma coisa que você pode fazer é mudar Sortpara que ele use melhor as 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);
}

Agora você pode especificar o campo a ser classificado ao chamar o Sortmétodo.

Sort(ref employees, e => e.DOB, SortDirection.Descending);
Samuel
fonte
7
Como a coluna de classificação está em uma string, você ainda precisará de um bloco switch / if-else para determinar qual função deve passá-la.
Tvanfosson 06/04/09
1
Você não pode fazer essa suposição. Quem sabe como o código dele chama isso.
Samuel Samuel
3
Ele afirmou na pergunta que o "ordenar por propriedade" está em uma string. Eu só estou passando pela pergunta dele.
Tvanfosson 06/04/09
6
Eu acho que é mais provável porque é proveniente de um controle de classificação em uma página da Web que passa a coluna de classificação como um parâmetro de string. Esse seria o meu caso de uso, de qualquer maneira.
Tvanfosson 06/04/09
2
@tvanfosson - Você está certo, eu tenho um controle personalizado que tem a ordem e o nome do campo como uma string
DotnetDude
55

Você pode usar o Reflection para obter o valor da propriedade.

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

Onde TypeHelper tem um método estático como:

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

Você também pode consultar o LINQ dinâmico na biblioteca de exemplos do VS2008 . Você pode usar a extensão IEnumerable para converter a lista como um IQueryable e, em seguida, usar a extensão OrderBy de link dinâmico.

 list = list.AsQueryable().OrderBy( sortBy + " " + sortDirection );
tvanfosson
fonte
1
Embora isso resolva o problema dele, podemos afastá-lo do uso de uma string para classificá-la. Boa resposta, no entanto.
Samuel
Você pode usar linq dinâmico sem Linq para Sql para fazer o que ele precisa ... Eu amo isso
JoshBerke
Certo. Você pode convertê-lo para IQueryable. Não pensei nisso. Atualizando minha resposta.
Tvanfosson 06/04/09
@ Samuel Se a classificação estiver chegando como uma variável de rota, não há outra maneira de classificá-la.
Chev
1
@ChuckD - traga a coleção para a memória antes de tentar usá-la, por exemplocollection.ToList().OrderBy(x => TypeHelper.GetPropertyValue( x, sortBy)).ToList();
tvanfosson
20

Foi assim que resolvi meu problema:

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
fonte
16

A construção da ordem por expressão pode ser lida aqui

Roubado descaradamente da página no link:

// 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
fonte
Há problemas associados a isso: Classificação de DateTime.
31410 CrazyEnigma
E quanto às classes compostas, como Person.Employer.CompanyName?
davewilliams459
Eu estava essencialmente fazendo a mesma coisa e esta resposta resolveu.
31412 Jason.Net
8

Você pode usar a reflexão para acessar a propriedade.

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

Notas

  1. Por que você passa na lista por referência?
  2. Você deve usar uma enumeração para a direção da classificação.
  3. Você poderia obter uma solução muito mais limpa se passasse uma expressão lambda especificando a propriedade a ser classificada em vez do nome da propriedade como uma sequência.
  4. Na minha lista de exemplos == null causará uma NullReferenceException, você deve pegar esse caso.
Daniel Brückner
fonte
Alguém já reparou que este é um tipo de retorno nulo, mas retorna listas?
EMD
Pelo menos ninguém se importou em corrigi-lo e eu não o notei porque não escrevi o código usando um IDE. Obrigado por apontar isso.
Daniel Brückner 15/06
6

A classificação usa a interface IComparable, se o tipo a implementar. E você pode evitar os ifs implementando um IComparer personalizado:

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

e depois

list.Sort(new EmpComp(sortBy));
Serguei
fonte
FYI: Sort é um método da Lista <T> e não é uma extensão do Linq.
06430 Serguei
5

Resposta 1:

Você poderá criar manualmente uma árvore de expressão que possa ser passada para OrderBy usando o nome como uma sequência. Ou você pode usar a reflexão como sugerido em outra resposta, o que pode ser menos trabalhoso.

Edit : Aqui está um exemplo de trabalho de construção de uma árvore de expressão manualmente. (Classificação em X.Value, quando apenas se conhece o nome "Valor" da propriedade). Você poderia (deveria) criar um método genérico para fazer isso.

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

Construir uma árvore de expressão requer que você conheça os tipos de particpação, no entanto. Isso pode ou não ser um problema no seu cenário de uso. Se você não souber em que tipo deve classificar, será mais fácil usar a reflexão.

Resposta para 2 .:

Sim, uma vez que o Comparador <T> .Default será usado para a comparação, se você não definir explicitamente o comparador.

driis
fonte
Você tem um exemplo de construção de uma árvore de expressão a ser passada para 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();
    }

}

Outro, desta vez para qualquer 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)));
    }

}

Você pode passar vários critérios de classificação, assim:

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

Infelizmente, a solução fornecida pelo Rashack não funciona para tipos de valor (int, enumerações etc.).

Para que ele funcione com qualquer tipo de propriedade, esta é a solução que encontrei:

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
fonte
Isso é incrível e é traduzido corretamente para SQL!
Xavier Poinas
1

Adicionando o que @Samuel e @bluish fizeram. Isso é muito mais curto, pois o Enum era desnecessário neste caso. Além disso, como um bônus adicional quando Ascendente for o resultado desejado, você pode passar apenas 2 parâmetros em vez de 3, pois true é a resposta padrão para o terceiro parâmetro.

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
fonte
0

Se você obtém o nome da coluna de classificação e a direção da classificação como string e não deseja usar a sintaxe switch ou if \ else para determinar a coluna, este exemplo pode ser interessante para você:

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

Solução baseada no uso do Dictionary que conecta as colunas necessárias para classificar via Expressão> e sua sequência de teclas.

Online123321
fonte