Сортування списку за допомогою 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.