Сортування списку за допомогою Lambda / Linq до об'єктів


274

У мене є рядок "сортувати за властивістю". Мені потрібно буде використовувати 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. Чи відомо про тип даних?


Я бачу sortBy == "FirstName" . ОП означає замість цього зробити .Equals () ?
— Пітер

3
@Pieter він, мабуть, мав на увазі порівняння рівності, але я сумніваюся, що він "мав робити .Equals ()". Typo зазвичай не призводить до коду, який функціонує.
— C.Evenhuis

1
@Pieter Ваше питання має сенс лише в тому випадку, якщо ви думаєте, що щось не так з ==... що?
— Джим Балтер

Відповіді:


365

Це можна зробити як

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

Рамка .NET передає лямбда (emp1,emp2)=>intяк aComparer<Employee>.

Це має перевагу в сильному наборі.


Часто мені траплялося писати складні оператори порівняння, що включають декілька критеріїв порівняння та несправне безпечне порівняння 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) тощо
— Chris Hynes

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
Оскільки стовпець сортування знаходиться в рядку, вам все одно знадобиться блок перемикання / 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, щоб передати список як IQueryable, а потім використовувати розширення Dynamic link OrderBy.

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

1
Хоча це і вирішує його проблему, ми можемо захотіти відмовити його від використання рядка для сортування. Хороша відповідь ні на що.
— Самуїл

Ви можете використовувати Dynamic linq без Linq для Sql, щоб робити те, що йому потрібно ... Я люблю це
— JoshBerke

Звичайно. Ви можете конвертувати його в IQueryable. Не думав про це. Оновлення моєї відповіді.
— tvanfosson

@Samuel Якщо сортування надходить як змінна маршруту, іншого способу його сортування не існує.
— Шев

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

З цим пов’язані проблеми: DateTime сортування.
— CrazyEnigma

А як щодо композитних класів, тобто Person.E 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. Ви повинні використовувати enum для напрямку сортування.
  3. Ви можете отримати набагато більш чисте рішення, якщо ви передасте лямбда-вираз із зазначенням властивості для сортування замість імені властивості як рядка.
  4. У моєму прикладі списку == null спричинить NullReferenceException, вам слід вчинити цей випадок.

Хто-небудь ще коли-небудь помічав, що це тип повернення недійсний, але повертає списки?
— emd

Принаймні, ніхто не піклувався про її виправлення, і я цього не помічав, бо не писав код за допомогою IDE. Дякуємо, що вказали на це.
— Даніель Брюкнер

6

Sort використовує інтерфейс 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));

FYI: Сортування - це метод 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 .:

Так, оскільки Порівняння <T> .Default буде використовуватися для порівняння, якщо ви чітко не визначаєте компаратор.


Чи є у вас приклад побудови дерева виразів для передачі в 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!
— Xavier Poinas

1

Додавши до того, що зробили @Samuel та @bluish. Це набагато коротше, оскільки Енум у цьому випадку був непотрібним. Плюс як додатковий бонус, коли бажаний результат - «Висхідний», ви можете передавати лише 2 параметри замість 3, оскільки істинним є відповідь за замовчуванням на третій параметр.

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

0

Якщо ви отримуєте назву стовпця сортування та сортуєте напрямок як рядок і не хочете використовувати перемикач чи синтаксис \ 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;
    }

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.