Ordinamento di un elenco usando Lambda / Linq per gli oggetti


276

Ho il nome del "ordina per proprietà" in una stringa. Dovrò usare Lambda / Linq per ordinare l'elenco degli oggetti.

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. Invece di usare un sacco di if per controllare il fieldname (sortBy), c'è un modo più pulito di fare l'ordinamento
  2. L'ordinamento è a conoscenza del tipo di dati?


Vedo sortBy == "FirstName" . Invece l'OP intendeva fare .Equals () ?
Pieter,

3
@Pieter probabilmente intendeva confrontare l'uguaglianza, ma dubito che "intendesse fare .Equals ()". Gli errori di battitura di solito non danno luogo a codice che funzioni.
C.Evenhuis,

1
@Pieter La tua domanda ha senso solo se pensi che ci sia qualcosa di sbagliato in ==... cosa?
Jim Balter,

Risposte:


367

Questo può essere fatto come

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

Il framework .NET sta lanciando la lambda (emp1,emp2)=>intcome aComparer<Employee>.

Questo ha il vantaggio di essere fortemente tipizzato.


Mi è capitato spesso di scrivere operatori di confronto complessi, che includevano criteri di confronto multipli e un confronto GUID fail-safe alla fine per garantire l'antisimmetria. Utilizzeresti un'espressione lambda per un confronto complesso come quello? In caso contrario, ciò significa che i confronti delle espressioni lambda dovrebbero essere limitati a casi semplici?
Simone,

4
sì, non lo vedo o qualcosa del genere? list.Sort ((emp1, emp2) => emp1.GetType (). GetProperty (sortBy) .GetValue (emp1, null) .CompareTo (emp2.GetType (). GetProperty (sortBy) .GetValue (emp2, null))) ;
Sab

1
come ordinare al contrario?
JerryGoyal,

1
@JerryGoyal scambia i parametri ... emp2.FirstName.CompareTo (emp1.FirstName) ecc.
Chris Hynes,

3
Solo perché è un riferimento di funzione non deve essere un solo liner. Potresti semplicemente scriverelist.sort(functionDeclaredElsewhere)
The Hoff,

74

Una cosa che potresti fare è cambiare in Sortmodo da utilizzare meglio le lambda.

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

Ora puoi specificare il campo da ordinare quando chiami il Sortmetodo.

Sort(ref employees, e => e.DOB, SortDirection.Descending);

7
Poiché la colonna di ordinamento si trova in una stringa, sarà comunque necessario un blocco switch / if-else per determinare quale funzione deve passare.
tvanfosson,

1
Non puoi fare questo presupposto. Chissà come lo chiama il suo codice.
Samuel,

3
Ha affermato nella domanda che "ordina per proprietà" è in una stringa. Sto solo rispondendo alla sua domanda.
tvanfosson,

6
Penso che sia più probabile perché proviene da un controllo di ordinamento su una pagina Web che passa indietro la colonna di ordinamento come parametro stringa. Sarebbe comunque il mio caso d'uso.
tvanfosson,

2
@tvanfosson - Hai ragione, ho un controllo personalizzato che ha l'ordine e il nome del campo come stringa
DotnetDude

55

È possibile utilizzare Reflection per ottenere il valore della proprietà.

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

Dove TypeHelper ha un metodo statico come:

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

Potresti anche voler dare un'occhiata a Dynamic LINQ dalla libreria degli esempi VS2008 . È possibile utilizzare l'estensione IEnumerable per trasmettere l'elenco come IQueryable e quindi utilizzare l'estensione OrderBy di collegamento dinamico.

 list = list.AsQueryable().OrderBy( sortBy + " " + sortDirection );

1
Mentre questo risolve il suo problema, potremmo voler allontanarlo dall'uso di una stringa per ordinarlo. Buona risposta comunque.
Samuel,

Puoi usare Dynamic Linq senza Linq per Sql per fare ciò di cui ha bisogno ... Lo adoro
JoshBerke,

Sicuro. Puoi convertirlo in IQueryable. Non ci ho pensato. Aggiornamento della mia risposta.
tvanfosson,

@Samuel Se l'ordinamento sta arrivando come variabile di percorso non c'è altro modo per ordinarlo.
Chev

1
@ChuckD - porta la raccolta in memoria prima di tentare di usarla, ad esempiocollection.ToList().OrderBy(x => TypeHelper.GetPropertyValue( x, sortBy)).ToList();
tvanfosson,

20

Ecco come ho risolto il mio 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();
}

16

Costruire l'ordine per espressione può essere letto qui

Spudoratamente rubato dalla pagina nel 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();

Esistono problemi associati a questo: ordinamento DateTime.
CrazyEnigma,

Inoltre che ne dite di classi composte, ad esempio Person.Employer.CompanyName?
davewilliams459,

Stavo essenzialmente facendo la stessa cosa e questa risposta l'ha risolta.
Jason.Net,

8

È possibile utilizzare la riflessione per accedere alla proprietà.

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

Appunti

  1. Perché passi l'elenco per riferimento?
  2. Dovresti usare un enum per la direzione dell'ordinamento.
  3. Potresti ottenere una soluzione molto più pulita se passassi un'espressione lambda specificando la proprietà da ordinare invece del nome della proprietà come stringa.
  4. Nel mio elenco di esempi == null causerà una NullReferenceException, dovresti prendere questo caso.

Qualcun altro ha mai notato che questo è un tipo restituito nullo ma restituisce elenchi?
emd

Almeno a nessuno importava ripararlo e non me ne sono accorto perché non ho scritto il codice usando un IDE. Grazie per la segnalazione.
Daniel Brückner,

6

L'ordinamento utilizza l'interfaccia IComparable, se il tipo la implementa. E puoi evitare gli if implementando un IComparer personalizzato:

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 poi

list.Sort(new EmpComp(sortBy));

FYI: Sort è un metodo di List <T> e non è un'estensione Linq.
Serguei,

5

Risposta per 1 .:

Dovresti essere in grado di costruire manualmente un albero delle espressioni che può essere passato in OrderBy usando il nome come stringa. O potresti usare la riflessione come suggerito in un'altra risposta, che potrebbe essere meno lavoro.

Modifica : ecco un esempio pratico di costruzione manuale di un albero di espressioni. (Ordinamento su X.Value, quando si conosce solo il nome "Valore" della proprietà). Potresti (dovresti) costruire un metodo generico per farlo.

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

La creazione di un albero delle espressioni richiede tuttavia di conoscere i tipi di partecipazione. Questo potrebbe o non potrebbe essere un problema nel tuo scenario di utilizzo. Se non sai su quale tipo dovresti ordinare, sarà probabilmente più facile usare la riflessione.

Risposta per 2 .:

Sì, poiché Comparer <T> .Default verrà utilizzato per il confronto, se non si definisce in modo esplicito il comparatore.


Hai un esempio di costruzione di un albero delle espressioni da passare in 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 altro, questa volta per qualsiasi 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)));
    }

}

Puoi passare più criteri di ordinamento, in questo modo:

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

4

Sfortunatamente, la soluzione fornita da Rashack non funziona per tipi di valore (int, enum, ecc.).

Per farlo funzionare con qualsiasi tipo di proprietà, questa è la soluzione che ho trovato:

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

È fantastico e persino tradotto correttamente in SQL!
Xavier Poinas il

1

Aggiungendo a ciò che hanno fatto @Samuel e @bluish. Questo è molto più breve in quanto l'Enum non era necessario in questo caso. Inoltre come bonus aggiuntivo quando l'Ascendente è il risultato desiderato, puoi passare solo 2 parametri invece di 3 poiché true è la risposta predefinita al terzo parametro.

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

0

Se ottieni il nome della colonna di ordinamento e la direzione di ordinamento come stringa e non vuoi usare switch o la sintassi if \ else per determinare la colonna, allora questo esempio potrebbe essere interessante per te:

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

Soluzione basata sull'uso del dizionario che si collega necessario per la colonna di ordinamento tramite Expression> e la sua stringa di chiavi.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.