65.9K
CodeProject 正在变化。 阅读更多。
Home

表达式树基础

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.74/5 (46投票s)

2011年8月4日

CPOL

8分钟阅读

viewsIcon

151650

downloadIcon

1773

使用 C# 表达式树构建自定义排序例程

引言

我希望读者熟悉 C# 泛型和反射。

几天前,在我目前工作的项目中,我遇到了一个有趣的问题。问题是根据某个属性对对象列表进行排序。

假设我们有一个 Person

public class Person
{
	public string Name { get; set; }
	public int Age { get; set; }
	public DateTime JoiningDate { get; set; }
}

以及一个 Person 对象列表

Person p1 = new Person { Name = "Kayes", Age = 29, JoiningDate = DateTime.Parse("2010-06-06") };
Person p2 = new Person { Name = "Gibbs", Age = 34, JoiningDate = DateTime.Parse("2008-04-23") };
Person p3 = new Person { Name = "Steyn", Age = 28, JoiningDate = DateTime.Parse("2011-02-17") };

List<Person> persons = new List<Person>();
persons.Add(p1);
persons.Add(p2);
persons.Add(p3);

假设我们需要按 Age 升序对 persons 列表进行排序。现在,任何 C# 3.0 新手(比如我)都会这样做:

List<Person> sortedList = persons.OrderBy<Person, int>(p => p.Age).ToList();

OrderBy() 会按 Age 属性升序对列表进行排序,persons 列表看起来会像这样:

ExpressionTreeBasics/ExpressionTreeBasics01.jpg

那么问题出在哪里?这里一切正常!对。问题出在我意识到我需要按哪个属性对列表进行排序是由运行时决定的。怎么说?我的 persons 列表被投影到一个 HTML gridview 中,其中 Person 对象的每个属性都显示在网格的每一列中。现在,我的客户希望通过单击列标题来按该列(即 Person 对象的相应属性)对网格数据进行排序。例如,客户单击 Age 列。因此,网格数据(person)将按 Age 属性排序。

当用户单击列时,我会在负责排序网格数据的代码中以 string 的形式获取属性名称。属性名称如何发送到后端代码超出了本文的范围。所以,我们只假设属性名称可用,我们只需要按该属性对 person 列表进行排序。

背景

当我们在上面编写 OrderBy() 调用来对列表进行排序时,我们通过泛型类型参数告诉方法按哪个属性类型对列表进行排序,并使用 lambda 表达式指定属性名称。因此,属性及其类型在编译时确定。但我们无法承担这样做,因为我们的 Person 对象中有 3 个不同数据类型的 3 个不同属性。用户可能希望按任何属性对网格进行排序。所以,我们必须在运行时确定属性的类型和名称。我们假设我们拥有属性名称。因此,识别类型并使用此信息对列表进行排序是剩下的工作。

然后表达式树就派上用场了。表达式树是 C# 3.0 引入的语言功能。它用于在运行时动态构建 lambda 表达式。有关表达式树的更多信息,请 在此处 查阅。

废话少说,直接进入代码

没错。这就是我们程序员活着的意义。代码。 ;)

我们需要构建我们上面用来调用 OrderBy() 方法的 lambda 表达式。也就是:

p => p.Age

这里的 p 是 lambda 表达式中传递的 Person 对象。所以,我们首先创建 ParameterExpressionParameterExpression 表示 lambda 表达式中 lambda (=>) 运算符左侧的输入变量。

ParameterExpression pe = Expression.Parameter(typeof(Person), "p");

我们给参数起的名称与我们在上面硬编码 lambda 表达式时所用的完全相同。现在来构建 lambda 表达式的主体。方法如下:

Expression<Func<Person, int>> expr = 
	Expression.Lambda<Func<Person, int>>(Expression.Property(pe, "Age"), pe);

让我们分析上面这行。首先,这个 Func<Person, int> 是什么,它被用作 Expression 类型的泛型类型参数?

答案:Func<Person, int> 是一个委托,它封装了一个方法,该方法有一个参数(Person)并返回第二个参数指定的类型(int)的值。签名是:

public delegate TResult Func<in T, out TResult>( T arg )

基本上,整个要点是我们要构建的表达式反映了一个由 Func<Person, int> 表示的方法,这就是我们在本文开头作为硬编码 lambda 表达式传递的内容。现在我们只是用表达式树来构建它。

但我们怎么知道委托是什么样的呢?也就是说,我们怎么知道委托会接受多少个输入参数,或者委托是否会返回任何值?很简单。问题是我们正在构建一个表达式,最终我们将用它来在 IEnumerable<T> 对象(即我们的 persons 列表)上调用 OrderBy() 扩展方法。OrderBy() 的签名如下:

public static IOrderedEnumerable<TSource> OrderBy<TSource, TKey>(
	this IEnumerable<TSource> source,
	Func<TSource, TKey> keySelector
)

所以这就是我们知道 keySelector 委托的样子。TSource 是我们要对其进行排序的对象的类型。TKey 是我们要用它来排序列表的属性的类型。让我们继续分析其余部分。

Expression<Func<Person, int>> expr = 
	Expression.Lambda<Func<Person, int>>(Expression.Property(pe, "Age"), pe);

Expression.Lambda() static 方法的第一个参数是主体表达式。在我们想要的 lambda 表达式中,主体只是返回我们通过参数访问的 Person 对象的属性。

我们将使用 Expression.Property() static 方法来创建主体表达式,它表示访问对象中的属性。我们将先前创建的 ParameterExpression pe 作为第一个参数传递给 Expression.Property() 方法,并将属性名称作为第二个参数。

然后再次将 ParameterExpression pe 作为第二个参数传递给 Expression.Lambda() 方法,以告知我们的 lambda 表达式的输入变量是什么。

这里有一个重要问题需要注意。在上面的代码片段中,我们在委托的泛型类型声明中显式类型创建了表达式。我们硬编码了要排序的列表的对象的类型(Person)。我们还硬编码了列表将按其排序的属性的类型(int)。列表类型在大多数情况下可能已知,但我们无法硬编码委托的返回类型,因为我们不知道列表将按哪种类型的属性排序。但稍后在文章中,我们将把我们的代码封装到一个泛型方法中。因此,我们将通过泛型类型参数获得列表类型和返回类型。但为了讨论的简单起见,让我们使用隐式类型 var 来创建表达式。

var expr = Expression.Lambda(Expression.Property(pe, "Age"), pe);

此时,我们的表达式就完成了,如果在 Visual Studio 中对 expr 进行快速监视,它看起来会是这样:

ExpressionTreeBasics/ExpressionTreeBasics02.jpg

与我们在开头硬编码的内容完全相同。现在让我们把到目前为止写的所有代码放在一起: 

Person p1 = new Person { Name = "Kayes", Age = 29, JoiningDate = DateTime.Parse("2010-06-06") };
Person p2 = new Person { Name = "Gibbs", Age = 34, JoiningDate = DateTime.Parse("2008-04-23") };
Person p3 = new Person { Name = "Steyn", Age = 28, JoiningDate = DateTime.Parse("2011-02-17") };

List<Person> persons = new List<Person>();
persons.Add(p1);
persons.Add(p2);
persons.Add(p3);

string sortByProp = "Age";
ParameterExpression pe = Expression.Parameter(typeof(Person), "p");
var expr = Expression.Lambda(Expression.Property(pe, sortByProp), pe);

请注意,我们将属性名称保留在一个变量中,并在构建表达式时使用了该变量。

现在我们所要做的就是在 OrderBy() 方法调用中使用该表达式。

List<Person> sortedList = persons.OrderBy<Person, int>(expr).ToList();

但等等…这是什么?

ExpressionTreeBasics/ExpressionTreeBasics03.jpg

看起来有错误

'System.Collections.Generic.List<CustomSort.Person>' does not contain a 
definition for 'OrderBy' and the best extension method overload 
'System.Linq.Queryable.OrderBy<TSource,TKey>(System.Linq.IQueryable<TSource>, 
System.Linq.Expressions.Expression<System.Func<TSource,TKey>>)' 
has some invalid arguments. 

这是因为我们的 persons 列表是 IEnumerable 类型,它没有接受表达式的 OrderBy() 重载。我们该怎么办?我们只需要将列表转换为 IQueryable<T>

IQueryable<Person> query = persons.AsQueryable();

然后对 IQueryable<Person> 调用 OrderBy()。 

List<Person> sortedList = query.OrderBy<Person, int>(expr).ToList();

因此,我们修改后的代码看起来如下:

Person p1 = new Person { Name = "Kayes", Age = 29, JoiningDate = DateTime.Parse("2010-06-06") };
Person p2 = new Person { Name = "Gibbs", Age = 34, JoiningDate = DateTime.Parse("2008-04-23") };
Person p3 = new Person { Name = "Steyn", Age = 28, JoiningDate = DateTime.Parse("2011-02-17") };

List<Person> persons = new List<Person>();
persons.Add(p1);
persons.Add(p2);
persons.Add(p3);

string sortByProp = "Age";
ParameterExpression pe = Expression.Parameter(typeof(Person), "p");
var expr = Expression.Lambda(Expression.Property(pe, sortByProp), pe);

IQueryable<Person> query = persons.AsQueryable();
List<Person> sortedList = query.OrderBy<Person, int>(expr).ToList();

所以我们的表达式可以与 OrderBy() 一起工作。但我们还没完成吗?还没完全。为什么?因为我们仍然将要排序列表的属性类型硬编码为 OrderBy() 方法的泛型类型参数。我们需要以某种方式在运行时确定此类型。方法是使用反射,如下所示:

Type sortByPropType = typeof(Person).GetProperty(sortByProp).PropertyType;

现在我们需要调用 OrderBy(),并传递存储在 sortByPropType 变量中的类型。这里也用到了反射。首先,我们需要获取 static 泛型 OrderBy() 方法的相应 MethodInfo 对象,该方法是定义在静态 Queryable 类中的扩展方法。

MethodInfo orderByMethodInfo = typeof(Queryable)
			.GetMethods(BindingFlags.Public | BindingFlags.Static)
			.Single(mi => mi.Name == "OrderBy"
			&& mi.IsGenericMethodDefinition
			&& mi.GetGenericArguments().Length == 2
			&& mi.GetParameters().Length == 2
		);

现在我们有了 OrderBy() 方法的 MethodInfo 对象,我们需要如下调用它:

List<Person> sortedList = (orderByMethodInfo.MakeGenericMethod(new Type[] 
	{ typeof(Person), sortByPropType }).Invoke(query, new object[] 
	{ query, expr }) as IOrderedQueryable<Person>).ToList();

我知道上面的反射代码肯定不好看 ;)

但就这样。我们的列表已按 Age 升序排序,结果存储在 sortedList 变量中。

最终代码

让我们看看最终的代码是什么样的: 

class Program
{
	static void Main(string[] args)
	{
		Person p1 = new Person { Name = "Kayes", Age = 29, JoiningDate = DateTime.Parse("2010-06-06") };
		Person p2 = new Person { Name = "Gibbs", Age = 34, JoiningDate = DateTime.Parse("2008-04-23") };
		Person p3 = new Person { Name = "Steyn", Age = 28, JoiningDate = DateTime.Parse("2011-02-17") };

		List<Person> persons = new List<Person>();
		persons.Add(p1);
		persons.Add(p2);
		persons.Add(p3);

		string sortByProp = "Age";
		Type sortByPropType = typeof(Person).GetProperty(sortByProp).PropertyType;
		
		ParameterExpression pe = Expression.Parameter(typeof(Person), "p");
		var expr = Expression.Lambda(Expression.Property(pe, sortByProp), pe);

		IQueryable<Person> query = persons.AsQueryable();
		
		MethodInfo orderByMethodInfo = 
		typeof(Queryable).GetMethods(BindingFlags.Public | BindingFlags.Static)
				.Single(mi => mi.Name == "OrderBy"
				&& mi.IsGenericMethodDefinition
				&& mi.GetGenericArguments().Length == 2
				&& mi.GetParameters().Length == 2
		);

		List<Person> sortedList = 
			(orderByMethodInfo.MakeGenericMethod(new Type[] 
			{ typeof(Person), sortByPropType }).Invoke(query, 
			new object[] { query, expr }) as 
			IOrderedQueryable<Person>).ToList();
	}
}

改进

虽然我们上面的代码工作正常,但它严重依赖于 Person 对象。所以如果需要,对于其他类型的对象列表,我们基本上需要编写相同的代码,但使用 Person 以外的其他对象。所以我想让代码可重用且通用,以便它可以适应我们提供的任何类型的对象。我们可以通过为 IEnumerable 类型创建一个扩展方法来做到这一点。

public static class MyExtensions
{
	public static List<T> CustomSort<T, TPropertyType>
	(this IEnumerable<T> collection, string propertyName, string sortOrder)
	{
		List<T> sortedlist = null;
		IQueryable<T> query = collection.AsQueryable<T>();

		ParameterExpression pe = Expression.Parameter(typeof(T), "p");
		Expression<Func<T, TPropertyType>> expr = Expression.Lambda<Func<T, TPropertyType>>(Expression.Property(pe, propertyName), pe);
		
		if (!string.IsNullOrEmpty(sortOrder) && sortOrder == "desc")
			sortedlist = query.OrderByDescending<T, TPropertyType>(expr).ToList();
		else
			sortedlist = query.OrderBy<T, TPropertyType>(expr).ToList();

		return sortedlist;
	}
} 

并通过反射以如下方式调用扩展方法:

List<Person> sortedList = typeof(MyExtensions).GetMethod("CustomSort").
	MakeGenericMethod(new Type[] { typeof(Person), sortByPropType }).
	Invoke(persons, new object[] { persons, sortByProp, "asc" }) as List<Person>;

如果需要,我们仍然可以像这样轻松地使用硬编码类型调用扩展方法:

sortedList = persons.CustomSort<Person, int>(sortByProp, "desc");

编辑 (2011-04-08)

感谢 Alois Kraus 指出无需将 IEnumerable<T> 转换为 IQueryable<T>,因为我们可以调用 Expression<TDelegate> 对象上的 Compile() 方法,该方法将表达式树描述的 lambda 表达式编译成可执行代码,并生成表示 lambda 表达式的委托。然后,我们可以将该委托传递给 IEnumerable<T>.OrderBy() 方法。因此,我们修改后的扩展方法应为:

public static class MyExtensions
{
	public static List<T> CustomSort<T, TPropertyType>
	(this IEnumerable<T> collection, string propertyName, string sortOrder)
	{
		List<T> sortedlist = null;
		
		ParameterExpression pe = Expression.Parameter(typeof(T), "p");
		Expression<Func<T, TPropertyType>> expr = Expression.Lambda<Func<T, TPropertyType>>(Expression.Property(pe, propertyName), pe);
		
		if (!string.IsNullOrEmpty(sortOrder) && sortOrder == "desc")
			sortedlist = collection.OrderByDescending<T, TPropertyType>(expr.Compile()).ToList();
		else
			sortedlist = collection.OrderBy<T, TPropertyType>(expr.Compile()).ToList();

		return sortedlist;
	}
}

结论

表达式树是 C# 3.0 引入的最酷的语言功能。并且与 Lambda 表达式(也是 C# 3.0 功能)一起,表达式树可以为有趣的可能性打开许多大门。我绝对期待在未来的项目中更多地使用这两者。

© . All rights reserved.