Sortowanie listy za pomocą Lambda / Linq do obiektów

274

Mam ciąg „sortuj według właściwości”. Będę musiał użyć Lambda / Linq do posortowania listy obiektów.

Dawny:

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. Zamiast używać paczki ifs do sprawdzania nazwy pola (sortBy), istnieje bardziej przejrzysty sposób sortowania
  2. Czy sort jest świadomy typu danych?
DotnetDude
źródło
3
Dupe: stackoverflow.com/questions/606997/…
Mehrdad Afshari
Widzę sortBy == „FirstName” . Czy OP chciał zamiast tego zrobić .Equals () ?
Pieter
3
@ Pieter prawdopodobnie miał na myśli porównanie równości, ale wątpię, żeby „zamierzał to zrobić .Equals ()”. Literówki zwykle nie powodują działania kodu.
C.Evenhuis
1
@ Pieter Twoje pytanie ma sens tylko wtedy, gdy uważasz, że coś jest nie tak z ==... czym?
Jim Balter,

Odpowiedzi:

365

Można to zrobić jako

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

Framework .NET rzuca lambda (emp1,emp2)=>intjakoComparer<Employee>.

Ma to tę zaletę, że jest mocno wpisane.

gls123
źródło
Często zdarzało mi się pisać skomplikowane operatory porównania, obejmujące wiele kryteriów porównania i bezpieczne GUID w celu zapewnienia antysymetrii. Czy użyłbyś wyrażenia lambda do takiego złożonego porównania? Jeśli nie, czy oznacza to, że porównania wyrażeń lambda powinny ograniczać się tylko do prostych przypadków?
Simone,
4
tak, nie widzę też czegoś takiego? list.Sort ((emp1, emp2) => emp1.GetType (). GetProperty (sortBy) .GetValue (emp1, null) .CompareTo (emp2.GetType (). GetProperty (sortBy) .GetValue (emp2, null))) ;
sob.
1
jak sortować w odwrotnej kolejności?
JerryGoyal,
1
@JerryGoyal zamieniają parametry ... emp2.FirstName.CompareTo (emp1.FirstName) itp.
Chris Hynes
3
Tylko dlatego, że jest to odwołanie do funkcji, nie musi to być jedna linijka. Możesz po prostu napisaćlist.sort(functionDeclaredElsewhere)
The Hoff
74

Jedną rzeczą, którą możesz zrobić, to zmienić, Sortaby lepiej wykorzystywać 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);
}

Teraz możesz określić pole do sortowania podczas wywoływania Sortmetody.

Sort(ref employees, e => e.DOB, SortDirection.Descending);
Samuel
źródło
7
Ponieważ kolumna sortowania znajduje się w ciągu, nadal potrzebujesz bloków switch / if-else, aby określić, która funkcja ma ją przekazać.
tvanfosson,
1
Nie możesz przyjąć tego założenia. Kto wie, jak nazywa go jego kod.
Samuel
3
Stwierdził w pytaniu, że „sortuj według właściwości” jest w postaci ciągu. Po prostu odpowiadam na jego pytanie.
tvanfosson,
6
Myślę, że jest to bardziej prawdopodobne, ponieważ pochodzi z kontrolki sortowania na stronie internetowej, która przekazuje kolumnę sortującą z powrotem jako parametr łańcucha. W każdym razie taki byłby mój przypadek użycia.
tvanfosson
2
@tvanfosson - Masz rację, mam niestandardową kontrolkę, która ma porządek i nazwę pola jako ciąg znaków
DotnetDude
55

Możesz użyć Reflection, aby uzyskać wartość właściwości.

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

Gdzie TypeHelper ma metodę statyczną, taką jak:

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

Możesz także zajrzeć na Dynamic LINQ z biblioteki próbek VS2008 . Możesz użyć rozszerzenia IEnumerable, aby rzutować Listę jako IQueryable, a następnie użyć rozszerzenia Dynamic Link OrderBy.

 list = list.AsQueryable().OrderBy( sortBy + " " + sortDirection );
tvanfosson
źródło
1
Chociaż to rozwiązuje jego problem, możemy chcieć odciągnąć go od używania sznurka do jego sortowania. Niemniej jednak dobra odpowiedź.
Samuel
Możesz użyć Dynamicznego linq bez Linq do Sql, aby zrobić to, czego potrzebuje ... Uwielbiam to
JoshBerke
Pewnie. Możesz przekonwertować go na IQueryable. Nie myślałem o tym. Aktualizacja mojej odpowiedzi.
tvanfosson
@Samuel Jeśli sortowanie nadchodzi jako zmienna trasy, nie ma innego sposobu na jego posortowanie.
Chev
1
@ChuckD - zapisz kolekcję do pamięci, zanim spróbujesz jej użyć, np.collection.ToList().OrderBy(x => TypeHelper.GetPropertyValue( x, sortBy)).ToList();
tvanfosson
20

Oto jak rozwiązałem mój problem:

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
źródło
16

Budowanie porządku przez wyrażenie można przeczytać tutaj

Bezwstydnie skradziony ze strony w linku:

// 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
źródło
Z tym związane są problemy: sortowanie według daty i godziny.
CrazyEnigma
A co z klasami złożonymi, tj. Person.Employer.CompanyName?
davewilliams459,
Zasadniczo robiłem to samo i ta odpowiedź rozwiązała ten problem.
Jason.Net,
8

Możesz użyć refleksji, aby uzyskać dostęp do nieruchomości.

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

Notatki

  1. Dlaczego podajesz listę przez odniesienie?
  2. Powinieneś użyć wyliczenia dla kierunku sortowania.
  3. Możesz uzyskać znacznie czystsze rozwiązanie, jeśli przekażesz wyrażenie lambda określające właściwość do sortowania zamiast nazwy właściwości jako ciąg.
  4. Na mojej przykładowej liście == null spowoduje wyjątek NullReferenceException, powinieneś złapać ten przypadek.
Daniel Brückner
źródło
Czy ktoś jeszcze zauważył, że jest to void typu return, ale zwraca listy?
emd
Przynajmniej nikt nie chciał tego naprawić i nie zauważyłem tego, ponieważ nie napisałem kodu przy użyciu IDE. Dzięki za zwrócenie na to uwagi.
Daniel Brückner,
6

Sort używa interfejsu IComparable, jeśli typ go implementuje. I możesz uniknąć ifs, wdrażając niestandardowy IComparer:

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

i wtedy

list.Sort(new EmpComp(sortBy));
Serguei
źródło
FYI: Sortowanie jest metodą z listy <T> i nie jest rozszerzeniem Linq.
Serguei
5

Odpowiedź na 1 .:

Powinieneś być w stanie ręcznie zbudować drzewo wyrażeń, które można przekazać do OrderBy, używając nazwy jako łańcucha. Lub możesz użyć refleksji, jak sugerowano w innej odpowiedzi, co może być mniej pracy.

Edycja : Oto działający przykład ręcznego budowania drzewa wyrażeń. (Sortowanie według X.Value, znając tylko nazwę „Wartość” właściwości). Możesz (powinieneś) zbudować ogólną metodę do tego.

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

Jednak zbudowanie drzewa wyrażeń wymaga znajomości typów cząstek. To może, ale nie musi stanowić problemu w twoim scenariuszu użytkowania. Jeśli nie wiesz, na jakim typie powinieneś sortować, łatwiej będzie użyć odbicia.

Odpowiedź na 2 .:

Tak, ponieważ porównywarka zostanie użyta do porównania, jeśli nie zdefiniujesz wyraźnie porównania.

driis
źródło
Czy masz przykład budowania drzewa wyrażeń, które ma być przekazywane do 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();
    }

}

Kolejny, tym razem dla każdego 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)));
    }

}

Możesz przekazać wiele kryteriów sortowania, takich jak to:

var q = dc.Felhasznalos.OrderBy(new string[] { "Email", "FelhasznaloID desc" });
Andras Vass
źródło
4

Rozwiązanie dostarczone przez Rashack nie działa niestety dla typów wartości (int, enums itp.).

Oto rozwiązanie, które znalazłem:

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
źródło
To jest niesamowite, a nawet poprawnie przetłumaczone na SQL!
Xavier Poinas
1

Dodanie do tego, co zrobili @Samuel i @bluish. Jest to o wiele krótsze, ponieważ Enum nie było w tym przypadku niepotrzebne. Dodatkowo jako dodatkowy bonus, gdy Rosnąco jest pożądanym wynikiem, możesz przekazać tylko 2 parametry zamiast 3, ponieważ prawda jest domyślną odpowiedzią na trzeci parametr.

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
źródło
0

Jeśli otrzymujesz nazwę kolumny sortowania i kierunek sortowania jako ciąg znaków i nie chcesz używać przełącznika lub składni if ​​\ else do określenia kolumny, to ten przykład może być dla Ciebie interesujący:

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

Rozwiązanie oparte na użyciu słownika, który łączy potrzebne do sortowania kolumny poprzez wyrażenie> i jej ciąg klucza.

Online123321
źródło