Сортировка List<T> по нескольким полям c учётом приоритета

616
30 января 2017, 15:58

Имеется List<T>, где T является каким-то классом с полями. Также имеется класс, содержащий параметры сортировки:

public class Sort
{
    public String Property { get; set; }
    public String Direction { get; set; }
}

Property указывает по какому полю будет осуществляться сортировка, а Direction указывает по возрастанию или убыванию (реально принимает значение "ASC" или "DSC")

Имеется метод, с сигнатурой

public static IEnumerable<T> Sort<T>(IEnumerable<T> items, Sort[] sort)

В массиве Sort[] чем меньше индекс элемента, тем выше его приоритет.

Хочется понять как можно осуществить сортировку по нескольким полям (больше двух), притом что явно не понятно в каком направлении и какое количество полей будет использоваться. Иными словами нельзя написать

items.OrderBy(...).ThenBy(...)

Для извлечения свойств из типа T используется typeof(T).GetProperty(sort[i].Property)

Answer 1

Примерно так:

public static IEnumerable<T> Sort<T>(IEnumerable<T> items, Sort[] sort)
{
    IOrderedEnumerable<T> temp = null;
    foreach (var s in sort)
    {
        Func<T, IComparable> keySelector = GetKeySelector<T>(s.Property);
        if (temp == null)
        {
            temp = s.Direction == "Asc" ?
                    items.OrderBy(keySelector) :
                    items.OrderByDescending(keySelector);
        }
        else
        {
            temp = s.Direction == "Asc" ?
                    temp.ThenBy(keySelector) :
                    temp.ThenByDescending(keySelector);
        }
    }
    return temp ?? items;
}
private static Func<T, IComparable> GetKeySelector<T>(string property)
{
    var param = Expression.Parameter(typeof(T));
    var lambda = Expression.Lambda<Func<T, IComparable>>(
        Expression.Convert(
            Expression.Property(param, property),
            typeof(IComparable)),
        param);
    return lambda.Compile();
}

можно убрать if-ы и добавить чуть-чуть динамики, вроде поиска метода ThenBy по имени - но на скорость работы это никак не повлияет.

Этот метод не идеален: он упадет при попытке сравнить по свойствам с типом, не реализующим IComparable (но, например, реализующим IComparable<T>). Для обхода проблемы в общем случае придется

  • или заменить в коде IComparable на object, получив боксинг при сравнении
  • или перейти на IQueryable - свалив проблему построения фунции сравнения на провайдер Linq To Objects, как сделано в ответе Andrei.

GetKeySelector - это просто способ сделать typeof(T).GetProperty(sort[i].Property) без медленного рефлекшена.

Answer 2

Кажется, это то, что вам нужно:

Сам метод будет выглядеть примерно так:

массив Sort нужно будет предварительно поменять, первый элемент должен быть OrderBy(OrderByDescending), остальные ThenBy(ThenByDescending)

 public static IEnumerable<T> SortIt<T>(IEnumerable<T> items, Sort[] sorts)
    {
        var queryItems = items.AsQueryable();
        foreach (var s in sorts)
            queryItems = queryItems.ApplyOrder(s.Property, s.Direction);
        return queryItems.AsEnumerable();
    } 

Ещё нужен экстеншен:

    public static class Extensions
   {
    public static IOrderedQueryable<T> ApplyOrder<T>(
        this IQueryable<T> source,
        string property,
        string methodName
    )
    {
        var arg = Expression.Parameter(typeof(T), "x");
        Expression expr = arg;
        expr = Expression.Property(expr, property);
        var lambda = Expression.Lambda(expr, arg);
        var method = typeof(Queryable).GetGenericMethod(
            methodName,
            new[] { typeof(T), expr.Type },
            new[] { source.GetType(), lambda.GetType() }
        );
        return (IOrderedQueryable<T>)method.Invoke(null, new object[] { source, lambda });
    }
    public static MethodInfo GetGenericMethod(
        this Type type,
        string name,
        Type[] genericTypeArgs,
        Type[] paramTypes
    )
    {
        var methods =
            from abstractGenericMethod
            in type.GetMethods(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static)
            where abstractGenericMethod.Name == name
            where abstractGenericMethod.IsGenericMethod
            let pa = abstractGenericMethod.GetParameters()
            where pa.Length == paramTypes.Length
            select abstractGenericMethod.MakeGenericMethod(genericTypeArgs) into concreteGenericMethod
            where concreteGenericMethod.GetParameters()
                .Select(p => p.ParameterType).SequenceEqual(paramTypes, new TestAssignable())
            select concreteGenericMethod;
        return methods.FirstOrDefault();
    }
    private class TestAssignable : IEqualityComparer<Type>
    {
        public bool Equals(Type x, Type y)
        {
            return x.IsAssignableFrom(y);
        }
        public int GetHashCode(Type obj)
        {
            return obj.GetHashCode();
        }
    }
}

https://habrahabr.ru/post/181065/

READ ALSO
Unity Удаление объектов (игра на подобие match 3)

Unity Удаление объектов (игра на подобие match 3)

Ситуация такая: пишу игру, на подобие match3, те 3 в ряд и ума не приложу как правильно сделать удаление ячейки и появление новых

513
textbox.Text=stack.Pop() не работает

textbox.Text=stack.Pop() не работает

Каким образом работает stackpop() ? Хочу реализовать функцию Undo

309