使用Lambda/Linq对对象进行排序

301

我有一个字符串表示“按属性排序”的名称。我需要使用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. 除了使用一堆if语句来检查字段名称(sortBy),是否有更简洁的方法进行排序?
  2. sort是否能够识别数据类型?

3
重复:https://dev59.com/ikbRa4cB1Zd3GeqP3cAd - Mehrdad Afshari
我看到 *sortBy == "FirstName"*。OP 是不是想用 .Equals() 代替了? - Pieter
3
他可能的确想比较相等性,但我怀疑他“想用.Equals()方法实现”。笔误通常不会导致代码可用。 - C.Evenhuis
2
@Pieter 只有当你认为 == 存在问题时,你的问题才有意义...什么问题? - Jim Balter
12个回答

412

这可以这样做:

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

.NET框架将 lambda表达式 (emp1, emp2) => int 强制转换为 Comparer<Employee>

这样做的好处是具有强类型。

如果您需要倒序/反向排序,请反转参数。

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

我经常写复杂的比较运算符,涉及多个比较标准和最后的安全GUID比较,以确保反对称性。你会在像这样的复杂比较中使用lambda表达式吗?如果不是,这是否意味着lambda表达式比较应仅限于简单情况? - Simone
4
我也看不到它,是这样的吗?list.Sort((emp1, emp2) => emp1.GetType().GetProperty(sortBy).GetValue(emp1, null).CompareTo(emp2.GetType().GetProperty(sortBy).GetValue(emp2, null))); - Sat
2
如何进行反向排序? - GorvGoyl
2
@JerryGoyal 交换参数... emp2.FirstName.CompareTo(emp1.FirstName) 等。 - Chris Hynes
5
函数引用并不一定要写成单行代码。你可以这样编写: list.sort(functionDeclaredElsewhere) - The Hoff
显示剩余4条评论

75

你可以做的一件事是改变代码中的 Sort 方法,让它更好地利用lambda表达式。

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
你不能做出那样的假设。谁知道他的代码如何调用它。 - Samuel
3
他在问题中说,“按属性排序”的方式是在一个字符串中的。我只是根据他的问题来回答。 - tvanfosson
6
我认为更可能是因为它来自于一个网页上的排序控件,该控件将排序列作为字符串参数传递回去。无论如何,这就是我的使用情况。 - tvanfosson
2
@tvanfosson - 你说得对,我有一个自定义控件,它将顺序和字段名作为字符串。 - DotnetDude
显示剩余2条评论

56

你可以使用反射来获取属性的值。

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

你可能还想看看来自VS2008示例库的动态LINQ。你可以使用IEnumerable扩展将List强制转换为IQueryable,然后使用动态链接OrderBy扩展。

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

1
虽然这解决了他的问题,但我们可能希望引导他不要使用字符串进行排序。无论如何,这是一个好答案。 - Samuel
@Samuel 如果排序是作为路由变量传入的,那么没有其他方法可以对其进行排序。 - Chev
@AlexFord 是的,有的。如果排序作为路由变量传入,那么你会得到一个字符串。你可以使用该字符串生成一个属性的 lambda 表达式。 - Jerry Joseph
LINQ to Entities 不认识 GetPropertyValue 方法,该方法无法转换为存储表达式。 - Chuck D
1
@ChuckD - 在尝试使用集合之前,将其加载到内存中,例如 collection.ToList().OrderBy(x => TypeHelper.GetPropertyValue( x, sortBy)).ToList(); - tvanfosson
显示剩余2条评论

23

这是我解决问题的方式:

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

可以在这里查看构建Order By表达式的方法:链接

无耻地从链接页面偷来:

// 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.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. 如果您通过 lambda 表达式传递指定要排序的属性而不是属性名称字符串,可以获得更清晰的解决方案。
  4. 在我的示例中,list == null 会导致 NullReferenceException,您应该捕获此情况。

有没有其他人注意到这是一个返回类型为void但返回列表的情况? - emd
至少没有人关心修复它,而且我也没有注意到,因为我没有使用集成开发环境编写代码。感谢指出这个问题。 - Daniel Brückner

6

如果类型实现了IComparable接口,Sort将使用它。通过实现自定义的IComparer,您可以避免使用ifs:

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:Sort 是 List<T> 的一个方法,而不是 Linq 扩展。 - Serguei

5

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
感谢您提供Expression.MakeUnary代码示例。这正是我正在寻找的。 - Frinavale

5

1.的答案:

你应该能够手动构建一个表达式树,将其作为字符串传递给OrderBy。 或者你可以像另一个答案中建议的那样使用反射,这可能会减少工作量。

编辑:这里是手动构建表达式树的可行示例。 (在只知道属性名称“Value”时按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; }
    }
}

构建表达式树需要您了解参与的类型,但这可能或可能不是您使用场景中的问题。如果您不知道应该按哪种类型进行排序,则使用反射可能会更容易。

第二个问题的答案:

是的,如果您没有明确定义比较器,将使用Comparer<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" });

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接