Сортировка списка с помощью Lambda / Linq по объектам

276

У меня есть имя "сортировать по свойству" в строке. Мне нужно будет использовать Lambda / Linq для сортировки списка объектов.

Пример:

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. Вместо использования множества ifs для проверки имени поля (sortBy), есть ли более чистый способ сортировки
  2. Сортировка осведомлена о типе данных?
DotnetDude
источник
3
Dupe: stackoverflow.com/questions/606997/…
Мехрдад Афшари
Я вижу sortBy == "FirstName" . OP хотел сделать вместо этого .Equals () ?
Питер
3
@ Питер, он, вероятно, действительно хотел сравнить равенство, но я сомневаюсь, что он "хотел сделать .Equals ()". Опечатки обычно не приводят к коду, который функционирует.
C.Evenhuis
1
@Pieter Ваш вопрос имеет смысл, только если вы думаете, что с ним ==что- то не так ... что?
Джим Балтер

Ответы:

367

Это можно сделать как

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

.NET Framework использует лямбду (emp1,emp2)=>intкакComparer<Employee>.

Это имеет преимущество того, чтобы быть строго напечатанным.

gls123
источник
Мне часто приходилось писать сложные операторы сравнения, включающие в себя несколько критериев сравнения и безотказное сравнение GUID в конце для обеспечения антисимметрии. Вы бы использовали лямбда-выражение для такого сложного сравнения? Если нет, означает ли это, что сравнения лямбда-выражений должны ограничиваться только простыми случаями?
Симона
4
да, я не вижу ничего подобного? list.Sort ((emp1, emp2) => emp1.GetType (). GetProperty (sortBy) .GetValue (emp1, null) .CompareTo (emp2.GetType (). GetProperty (sortBy) .GetValue (emp2, null))) ;
Суббота,
1
как сортировать в обратном порядке?
JerryGoyal
1
@JerryGoyal поменяйте местами параметры ... emp2.FirstName.CompareTo (emp1.FirstName) и т. Д.
Крис Хайнс,
3
Просто потому, что это ссылка на функцию, это не должно быть одной строкой. Вы могли бы просто написатьlist.sort(functionDeclaredElsewhere)
The Hoff
74

Одна вещь, которую вы могли бы сделать, это изменить, Sortчтобы лучше использовать лямбды.

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

Теперь вы можете указать поле для сортировки при вызове Sortметода.

Sort(ref employees, e => e.DOB, SortDirection.Descending);
Самуил
источник
7
Поскольку столбец сортировки находится в строке, вам все равно понадобятся блоки switch / if-else, чтобы определить, какую функцию передать.
tvanfosson
1
Вы не можете сделать это предположение. Кто знает, как его код называет это.
Самуил
3
В вопросе он заявил, что «сортировать по свойству» в строке. Я просто иду по его вопросу.
tvanfosson
6
Я думаю, что это более вероятно, потому что это происходит от элемента управления сортировкой на веб-странице, который передает столбец сортировки обратно как строковый параметр. Во всяком случае, это был бы мой вариант использования.
tvanfosson
2
@ tvanfosson - Вы правы, у меня есть пользовательский элемент управления, в котором порядок и имя поля представлены в виде строки
DotnetDude
55

Вы можете использовать Reflection, чтобы получить значение свойства.

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

Где TypeHelper имеет статический метод, такой как:

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

Вы также можете посмотреть на Dynamic LINQ из библиотеки образцов VS2008 . Вы можете использовать расширение IEnumerable, чтобы преобразовать List в IQueryable, а затем использовать динамическое расширение OrderBy.

 list = list.AsQueryable().OrderBy( sortBy + " " + sortDirection );
tvanfosson
источник
1
Хотя это и решает его проблему, мы можем отвести его от использования строки для сортировки. Хороший ответ тем не менее.
Самуил
Вы можете использовать Dynamic linq без Linq to Sql, чтобы делать то, что ему нужно ... Мне это нравится
JoshBerke
Конечно. Вы можете конвертировать его в IQueryable. Не думал об этом. Обновление моего ответа.
tvanfosson
@Samuel Если сортировка входит в качестве переменной маршрута, другого способа сортировки нет.
Chev
1
@ChuckD - перенесите коллекцию в память, прежде чем пытаться ее использовать, напримерcollection.ToList().OrderBy(x => TypeHelper.GetPropertyValue( x, sortBy)).ToList();
tvanfosson
20

Вот как я решил свою проблему:

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

Построение порядка по выражению можно прочитать здесь

Бесстыдно похищенный со страницы в ссылке:

// 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
источник
Есть проблемы, связанные с этим: сортировка DateTime.
CrazyEnigma
Также как насчет составных классов, т.е. Person.Employer.CompanyName?
davewilliams459
По сути, я делал то же самое, и этот ответ решил его.
Jason.Net
8

Вы можете использовать отражение для доступа к свойству.

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

Ноты

  1. Почему вы передаете список по ссылке?
  2. Вы должны использовать перечисление для направления сортировки.
  3. Вы могли бы получить более чистое решение, если бы передали лямбда-выражение, определяющее свойство для сортировки, вместо имени свойства в виде строки.
  4. В моем примере list == null вызовет исключение NullReferenceException, вы должны поймать этот случай.
Даниэль Брюкнер
источник
Кто-нибудь еще когда-нибудь замечал, что это возвращаемый тип void, но возвращает списки?
EMD
По крайней мере, никто не хотел исправить это, и я не заметил этого, потому что я не писал код, используя IDE. Спасибо что подметил это.
Даниэль Брюкнер
6

Сортировка использует интерфейс IComparable, если тип реализует его. И вы можете избежать ifs, внедрив собственный 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
    }
}

а потом

list.Sort(new EmpComp(sortBy));
Сергей
источник
К вашему сведению: Sort - это метод List <T>, а не расширение Linq.
Сергей
5

Ответ за 1 .:

Вы должны быть в состоянии вручную построить дерево выражений, которое можно передать в OrderBy, используя имя в качестве строки. Или вы можете использовать рефлексию, как предложено в другом ответе, что может быть меньше работы.

Изменить : Вот рабочий пример построения дерева выражений вручную. (Сортировка по X.Value, когда известно только имя «Значение» свойства). Вы можете (должны) создать универсальный метод для этого.

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

Однако для построения дерева выражений вам необходимо знать конкретные типы. Это может или не может быть проблемой в вашем сценарии использования. Если вы не знаете, по какому типу вы должны сортировать, возможно, будет легче использовать рефлексию.

Ответ за 2 .:

Да, так как Comparer <T> .Default будет использоваться для сравнения, если вы явно не определите компаратор.

driis
источник
У вас есть пример построения дерева выражений для передачи в 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();
    }

}

Еще один, на этот раз для любого 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)));
    }

}

Вы можете пройти несколько критериев сортировки, например так:

var q = dc.Felhasznalos.OrderBy(new string[] { "Email", "FelhasznaloID desc" });
Андрас Васс
источник
4

Решение, предоставленное Rashack, к сожалению, не работает для типов значений (int, enums и т. Д.).

Чтобы оно работало с любым типом собственности, я нашел следующее решение:

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;
    }
Антуан Жассойн
источник
Это потрясающе и даже правильно переводится на SQL!
Ксавье Поинас
1

Добавление к тому, что сделали @Samuel и @bluish. Это намного короче, так как Enum в этом случае не нужен. Плюс в качестве дополнительного бонуса, когда Ascending является желаемым результатом, вы можете передать только 2 параметра вместо 3, поскольку true является ответом по умолчанию для третьего параметра.

public void Sort<TKey>(ref List<Person> list, Func<Person, TKey> sorter, bool isAscending = true)
{
    list = isAscending ? list.OrderBy(sorter) : list.OrderByDescending(sorter);
}
Стивен Уитлок
источник
0

Если вы получаете имя столбца сортировки и направление сортировки в виде строки и не хотите использовать переключатель или синтаксис if \ else для определения столбца, то этот пример может быть вам интересен:

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

Решение основано на использовании словаря, который соединяет необходимый для сортировки столбец через выражение> и его ключевую строку.

Online123321
источник