【问题标题】:Better way to Sort a List by any property按任何属性对列表进行排序的更好方法
【发布时间】:2015-08-15 03:32:51
【问题描述】:

我的方法接收所有 DataTables 参数以按单击的列对表进行排序。我从每个页面列表的控制器调用此方法。 我正在寻找一种更好的方法来执行此操作,例如适用于所有类型的通用方法:stringintdecimaldouble bool可为空不可)。但我找不到。

我当前的代码:

public List<T> OrderingList<T>(List<T> list, DataTablesParam model)
{
    var iColumn = model.Order.FirstOrDefault().Column;
    var property = typeof(T).GetProperty(model.Columns.ToArray()[iColumn].Data);
    var param = Expression.Parameter(typeof(T));
    var final = Expression.Property(param, property);

    var isDirAsc = model.Order.FirstOrDefault().Dir.Equals("asc");

    if (property.PropertyType == typeof(string))
    {
        var lambda = Expression.Lambda<Func<T, string>>(final, param).Compile();
        return isDirAsc ? list.OrderBy(lambda).ToList() : list.OrderByDescending(lambda).ToList();
    }
    else if (property.PropertyType == typeof(int))
    {
        var lambda = Expression.Lambda<Func<T, int>>(final, param).Compile();
        return isDirAsc ? list.OrderBy(lambda).ToList() : list.OrderByDescending(lambda).ToList();
    }
    else if (property.PropertyType == typeof(bool))
    {
        var lambda = Expression.Lambda<Func<T, bool>>(final, param).Compile();
        return isDirAsc ? list.OrderBy(lambda).ToList() : list.OrderByDescending(lambda).ToList();
    }
    else if (property.PropertyType == typeof(decimal))
    {
        var lambda = Expression.Lambda<Func<T, decimal>>(final, param).Compile();
        return isDirAsc ? list.OrderBy(lambda).ToList() : list.OrderByDescending(lambda).ToList();
    }
    else if (property.PropertyType == typeof(double))
    {
        var lambda = Expression.Lambda<Func<T, double>>(final, param).Compile();
        return isDirAsc ? list.OrderBy(lambda).ToList() : list.OrderByDescending(lambda).ToList();
    }

    return list;
}

我想做这样的事情:(但是这段代码不起作用)

public List<T> OrderingList<T>(List<T> list, DataTablesParam model)
{
    var iColumn = model.Order.FirstOrDefault().Column;
    var property = typeof(T).GetProperty(model.Columns.ToArray()[iColumn].Data);
    var param = Expression.Parameter(typeof(T));
    var final = Expression.Property(param, property);

    var isDirAsc = model.Order.FirstOrDefault().Dir.Equals("asc");

    var lambda = Expression.Lambda<Func<T, dynamic>>(final, param).Compile();
    return isDirAsc ? list.OrderBy(lambda).ToList() : list.OrderByDescending(lambda).ToList();
}

【问题讨论】:

  • 为什么不提供一个 lambda 来获取排序属性并使用 IComparer&lt;T&gt; 进行比较?
  • 你基本上会使用这个现有的方法:msdn.microsoft.com/en-us/library/vstudio/… - list.OrderBy(item =&gt; item.SomeProperty, Comparer&lt;PropertyType&gt;.Default); - 你可以创建一个默认提供比较器的方法。
  • 可能创建另一个通用方法 CreateLambda 返回 Expression.Lamda 。然后,您可以通过反射获取此 CreateLambda 的 MethodInfo 并将其转换为您的 TIn 和 Tout。

标签: c# generics reflection expression func


【解决方案1】:

您可以使用反射调用Enumerable.OrderBy 方法。这样,您不必在编译时知道类型。为此,您只需要获取方法,并使用属性的类型创建一个泛型方法:

private IEnumerable<T> Sort<T> (List<T> list, string propertyName)
{
    MethodInfo orderByMethod = typeof(Enumerable).GetMethods().First(mi => mi.Name == "OrderBy" && mi.GetParameters().Length == 2);

    PropertyInfo pi = typeof(T).GetProperty(propertyName);
    MethodInfo orderBy = orderByMethod.MakeGenericMethod(typeof(T), pi.PropertyType);

    ParameterExpression param = Expression.Parameter(typeof(T));
    Delegate accessor = Expression.Lambda(Expression.Property(param, pi), param).Compile();
    return (IEnumerable<T>)orderBy.Invoke(null, new object[] { lst, accessor });
}

请注意,我提取了有关您的模型的内容,以保持此方法足够通用。它基本上可以通过指定属性名称来按列表中的任何属性排序。您的原始方法将如下所示:

public List<T> OrderingList<T>(List<T> list, DataTablesParam model)
{
    var iColumn = model.Order.FirstOrDefault().Column;
    string propertyName = model.Columns.ToArray()[iColumn].Data;

    return Sort(list, propertyName).ToList();
}

【讨论】:

  • 编译 Lambda 会导致性能下降。在这种情况下,这可能很重要,也可能无关紧要。如果重要,可以缓存已编译的 lambda。
  • @EricJ。那是个很好的观点;该功能仍有一些优化空间。还应该只查找一次orderByMethod
【解决方案2】:

这对我来说很好用:(感谢@Poke)

https://stackoverflow.com/a/31393168/5112444

我的最终方法:

private IEnumerable<T> Sort<T>(IEnumerable<T> list, string propertyName, bool isAsc)
{
    MethodInfo orderByMethod = typeof(Enumerable).GetMethods().First(mi => mi.Name == (isAsc ? "OrderBy" : "OrderByDescending") && mi.GetParameters().Length == 2);

    PropertyInfo pi = typeof(T).GetProperty(propertyName);
    MethodInfo orderBy = orderByMethod.MakeGenericMethod(typeof(T), pi.PropertyType);

    ParameterExpression param = Expression.Parameter(typeof(T));
    Delegate accessor = Expression.Lambda(Expression.Call(param, pi.GetGetMethod()), param).Compile();
    return (IEnumerable<T>)orderBy.Invoke(null, new object[] { list, accessor });
}

【讨论】:

    【解决方案3】:

    您建议的方法几乎有效。您需要更改两件事才能使其正常工作:

    public List<T> OrderingList<T>(List<T> list, DataTablesParam model)
    {
        var iColumn = model.Order.FirstOrDefault().Column;
        var property = typeof(T).GetProperty(model.Columns.ToArray()[iColumn].Data);
        var param = Expression.Parameter(typeof(T), "p");
        Expression final = Expression.Property(param, property);
    
        // Boxing of value types
        if (property.PropertyType.IsValueType) {
            final = Expression.MakeUnary(ExpressionType.Convert, final, typeof(object));
        }
    
        var isDirAsc = model.Order.FirstOrDefault().Dir.Equals("asc");
    
        //                                     VVVVVV
        var lambda = Expression.Lambda<Func<T, object>>(final, param).Compile();
        return isDirAsc
            ? list.OrderBy(lambda).ToList()
            : list.OrderByDescending(lambda).ToList();
    }
    
    1. 不要使用dynamic,而是使用object,因为每个类型都是object
    2. 如果你有一个值类型,你需要一个装箱操作,即你必须将值转换为对象(object)i。这是通过一元转换操作完成的:

      Expression final = Expression.Property(param, property);
      if (property.PropertyType.IsValueType) {
          final = Expression.MakeUnary(ExpressionType.Convert, final, typeof(object));
      }
      

    另请注意,final 显式声明为 Expression,因为表达式类型可能会从属性变为一元表达式。

    【讨论】:

    • 这种情况抛出这个异常:无法将类型 System.Int32 转换为类型 System.Object。 LINQ to Entities 仅支持转换 EDM 原语或枚举类型@Olivier
    【解决方案4】:

    除了不是很通用之外,您的解决方案还需要大量额外内存,因为您正在使用 LINQ 复制列表。您可以使用List.Sort 来避免这种情况。

    我愿意:

    static void SortBy<T>(List<T> list, MemberInfo member, bool desc)
    {
        Comparison<T> cmp = BuildComparer<T>(member, desc);
        list.Sort(cmp);
    }
    
    static Comparison<T> BuildComparer<T>(MemberInfo member, bool desc)
    {
        var left = Expression.Parameter(typeof(T));
        var right = Expression.Parameter(typeof(T));
    
        Expression cmp = Expression.Call(
            Expression.MakeMemberAccess(desc ? right : left, member),
            "CompareTo",
            Type.EmptyTypes,
            Expression.MakeMemberAccess(desc ? left : right, member));
    
        return Expression.Lambda<Comparison<T>>(cmp, left, right).Compile();
    }
    

    【讨论】:

    • 这不起作用有多种原因:如果您打算使用IComparer&lt;&gt;,则该方法称为Compare,并且您需要一个实际的比较器实例来调用它。如果您打算使用IComparable&lt;&gt;,那么您将不会得到Comparer&lt;T&gt;,您可以将其传递给Sort 方法。最后,您需要传递 IComparer&lt;&gt;IComparable&lt;&gt; 的类型参数将是成员/属性类型,而不是列表项的类型,因为您想比较这些属性的值。
    • 谢谢,我把IComparable 打错了IComparer。它现在应该可以工作了。
    • 然后,根据我上面的评论,类型参数需要是成员的类型(而不是列表项类型 T),并且 IComparable&lt;&gt;.CompareTo() 返回一个 int,而不是 @987654334 @对象。
    • 通过内存而不是在 IDE 中测试的风险。叹息。
    • 哈哈,是的,这似乎工作得更好。不过,它似乎不适用于字符串属性。不过,您可以这样调用该方法:typeof(IComparable&lt;&gt;).MakeGenericType(member.PropertyType).GetMethod("CompareTo")(假设 member 现在是 PropertyInfo)。
    【解决方案5】:

    我找到了一个更好的方法来做到这一点。我必须做 3 个步骤:

    1 - 在项目中添加包“Linq Dynamic”:

    Install-Package System.Linq.Dynamic.Library
    

    2 - 在 Class 中导入包:

    using System.Linq.Dynamic;
    

    3 - 按属性的字符串名称排序列表:

    list.OrderBy(stringPropertyName);                 //asc
    list.OrderBy(stringPropertyName + " descending"); //des
    

    它非常适合我。

    【讨论】:

    • 请注意,这与我的解决方案非常相似,但需要添加依赖项。我最初想建议使用 Linq Dynamic,但为了简单起见,我决定放弃,并向您展示如何使您的尝试奏效。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-08-18
    • 1970-01-01
    • 2020-12-16
    • 2011-05-13
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多