在Visual Studio 2010中使用表达式树生成动态方法
表达式树首次出现在Visual Studio 2008中,主要由LINQ提供程序使用。您可以使用表达式树以树状格式表示代码,其中每个节点都是一个表达式。您还可以将表达式树转换为已编译的代码并运行它。通过这种转换,可以对可执行代码进行动态修改,并可以在各种数据库中执行LINQ查询并创建动态查询。Charlie Calvert的博客文章Expression Tree Basics中解释了Visual Studio 2008中的表达式树。
在本文中,我将展示如何在Visual Studio 2010中扩展表达式树,以及如何使用它们生成动态方法(以前只能通过发出MSIL才能解决的问题)。但是,尽管我强烈建议您首先阅读Charlie的博客文章,但我仍然需要重复一些基础知识以阐明某些细微差别。
创建表达式树
生成表达式树的最简单方法是创建一个Expression <T>类型的实例,其中T是委托类型,并为该实例分配一个lambda表达式。让我们看一下代码。
//通过提供lambda表达式来创建表达式树。
Expression <Action <int >> printExpr =(arg)=> Console.WriteLine(arg);
//编译并调用表达式树。
printExpr.Compile()(10);
//打印10。
在此示例中,C#编译器从提供的lambda表达式生成表达式树。请注意,如果使用Action <int>代替Expression <Action <int >>作为printExpr对象的类型,则不会创建任何表达式树,因为委托不会转换为表达式树。
但是,这不是创建表达式树的唯一方法。您还可以使用System.LINQ.Expressions命名空间中的类和方法。例如,您可以使用以下代码创建相同的表达式树。
//为表达式树创建一个参数。
ParameterExpression param = Expression.Parameter(typeof(int),“ arg”);
//为方法调用创建一个表达式并指定其参数。
MethodCallExpression methodCall = Expression.Call(
typeof(Console).GetMethod(“ WriteLine”,new Type [] {typeof(int)}),
param
);
//编译并调用methodCall表达式。
Expression.Lambda<Action<int>>(
methodCall,
new ParameterExpression[] { param }
).Compile()(10);
//同时显示10。
当然,这看起来要复杂得多,但这是在向表达式树提供lambda表达式时实际发生的。
表达式树与Lambda表达式
一个常见的误解是表达式树与lambda表达式相同。这不是真的。一方面,正如我已经展示的,您可以使用API方法来创建和修改表达式树,而完全不使用lambda表达式语法。另一方面,并非每个lambda表达式都可以隐式转换为表达式树。例如,多行lambdas(也称为语句lambdas)不能隐式转换为表达式树。
//您可以在委托中使用多行lambda。
Action<int> printTwoLines = (arg) =>
{
Console.WriteLine("Print arg:");
Console.WriteLine(arg);
};
//但是在表达式树中,这会生成编译器错误。
Expression<Action<int>> printTwoLinesExpr = (arg) =>
{
Console.WriteLine("Print arg:");
Console.WriteLine(arg);
};
Visual Studio 2010中的表达树
到目前为止,我展示的所有代码示例在Visual Studio 2008和Visual Studio 2010中都可以工作(或不工作)。现在,让我们转到C#4.0和Visual Studio 2010。
在Visual Studio 2010中,扩展了表达式树API,并将其添加到动态语言运行库(DLR)中,以便语言实现者可以发出表达式树而不是MSIL。为了支持这个新目标,控制流和分配功能已添加到表达式树中:循环,条件块,try-catch块等。
有一个陷阱:您不能通过使用lambda表达式语法来“简单地”使用这些新功能。您必须使用表达式树API。因此,即使在Visual Studio 2010中,上一节中的最后一个代码示例仍然会生成编译器错误。
但是,现在您可以使用Visual Studio 2008中不提供的API方法来创建这种表达式树。这些方法之一是Expression.Block,它可以按顺序执行多个表达式,而这正是该方法我需要这个例子。
//为表达式树创建参数。
ParameterExpression param = Expression.Parameter(typeof(int),“ arg”);
//创建用于打印常量字符串的表达式。
MethodCallExpression firstMethodCall = Expression.Call(
typeof(Console).GetMethod(“ WriteLine”,new Type [] {typeof(String)}),
Expression.Constant(“ Print arg:”)
);
//创建用于打印传递的参数的表达式。
MethodCallExpression secondMethodCall = Expression.Call(
typeof(Console).GetMethod(“ WriteLine”,new Type [] {typeof(int)}),
param
);
//创建一个结合了两个方法调用的块表达式。
BlockExpression块= Expression.Block(firstMethodCall,secondMethodCall);
//编译并调用表达式树。
Expression.Lambda<Action<int>>(
block,
new ParameterExpression[] { param }
).Compile()(10);
我将重复一遍:尽管扩展了表达式树API,但是表达式树与lambda表达式语法一起工作的方式没有改变。这意味着Visual Studio 2010中的LINQ查询具有与Visual Studio 2008中相同的功能(和相同的限制)。
但是由于有了这些新功能,您可以在LINQ之外找到更多可以使用表达式树的区域。
生成动态方法
现在让我们转到新API可以提供帮助的实际问题上。最著名的一种是创建动态方法。解决此问题的常用方法是使用System.Reflection.Emit并直接与MSIL一起使用。不用说,生成的代码很难编写和阅读。
从本质上讲,将树两行打印到控制台的表达式树已经是动态方法的示例。但是,让我们尝试一些更复杂的示例,以演示新API的更多功能。感谢DLR团队的开发人员John Messerly提供了以下示例。
假设您有一个简单的方法来计算数字的阶乘。
static int CSharpFact(int value)
{
int result = 1;
while (value > 1)
{
result *= value--;
}
return result;
}
现在,您需要一种可以执行相同操作的动态方法。我们这里有几个基本元素:传递给方法的参数,局部变量和循环。这是通过使用表达式树API来表示这些元素的方式。
static Func<int, int> ETFact()
{
//创建参数表达式。
ParameterExpression value = Expression.Parameter(typeof(int), "value");
//创建一个表达式以保存局部变量。
ParameterExpression result = Expression.Parameter(typeof(int), "result");
//创建要从循环跳转到的标签。
LabelTarget label = Expression.Label(typeof(int));
//创建方法主体。
BlockExpression block = Expression.Block(
//添加局部变量。
new[] { result },
//将常量分配给局部变量:result = 1
Expression.Assign(result, Expression.Constant(1)),
//添加一个循环。
Expression.Loop(
//将条件块添加到循环中。
Expression.IfThenElse(
//条件:值> 1
Expression.GreaterThan(value, Expression.Constant(1)),
//如果为true:结果* =值-
Expression.MultiplyAssign(result,
Expression.PostDecrementAssign(value)),
//如果为false,则从循环退出并转到标签。
Expression.Break(label, result)
),
//跳转到的标签。
label
)
);
//编译表达式树并返回委托。
return Expression.Lambda<Func<int, int>>(block, value).Compile();
}
是的,这看起来比原始的C#代码更复杂,不清楚。但是,将其与生成MSIL所必须编写的内容进行比较。
static Func<int, int> ILFact()
{
var method = new DynamicMethod(
"factorial", typeof(int),
new[] { typeof(int) }
);
var il = method.GetILGenerator();
var result = il.DeclareLocal(typeof(int));
var startWhile = il.DefineLabel();
var returnResult = il.DefineLabel();
// result = 1
il.Emit(OpCodes.Ldc_I4_1);
il.Emit(OpCodes.Stloc, result);
// if (value <= 1) branch end
il.MarkLabel(startWhile);
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldc_I4_1);
il.Emit(OpCodes.Ble_S, returnResult);
// result *= (value--)
il.Emit(OpCodes.Ldloc, result);
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Dup);
il.Emit(OpCodes.Ldc_I4_1);
il.Emit(OpCodes.Sub);
il.Emit(OpCodes.Starg_S, 0);
il.Emit(OpCodes.Mul);
il.Emit(OpCodes.Stloc, result);
// end while
il.Emit(OpCodes.Br_S, startWhile);
// return result
il.MarkLabel(returnResult);
il.Emit(OpCodes.Ldloc, result);
il.Emit(OpCodes.Ret);
return (Func<int, int>)method.CreateDelegate(typeof(Func<int, int>));
}