使用 `System.Linq.Expression` 可以非常方便地在应用程序运行时动态生成 IL 代码,从而构造动态方法。相比于 `System.Reflection.Emit.ILGenerator`,使用表达式树构造动态方法要比使用 IL 代码更为人性化。至于原因嘛……只能说,复杂的函数用 IL 指令自己编写……我做不到 TT
当然,也可以使用 CodeDOM 来编写动态类型(以及动态方法),然而,由于 CodeDOM 相当于是根据 DOM图 生成了代码,然后调用编译器对代码重新进行编译,因此会存在一定的效率问题(c.f. Reflection.Emit vs CodeDOM)
还好,从 .NET Framework 4.0 开始,可以使用表达式树生成动态方法。例如,一个简单的输入日期/时间的例子
using System; using System.Linq.Expressions; namespace ConsoleApplication1 { class Program { static void Main(string[] args) { // 构造一个函数 DateTime f(string prompt); var argPrompt = Expression.Parameter(typeof(string), "propmpt"); // 局部变量。 var localInput = Expression.Parameter(typeof(string), "input"); var Console_WriteLine = typeof (Console).GetMethod("WriteLine", new[] {typeof (string)}); // 表达式块。 var expr = Expression.Block(typeof (DateTime), new[] {localInput}, Expression.Call(Console_WriteLine, argPrompt), Expression.Assign(localInput, Expression.Call(typeof (Console), "ReadLine", null)), Expression.Call(typeof (DateTime), "Parse", null, localInput)); // 根据表达式块生成 Lambda 函数。 var lambda = Expression.Lambda(expr, argPrompt); // 编译 Lambda 函数,生成委托。 var f = (Func<string, DateTime>) lambda.Compile(); Console.WriteLine(f("请输入一个日期/时间:")); } } }
运行结果如下
请输入一个日期/时间: 2000-1-1 PM 10:10:10 2000-1-1 22:10:10 请按任意键继续. . .
这样要比手动编写 IL 汇编更为容易。可以说是在动态方法上迈进了一大步。然而,使用动态生成的委托也为调试带来了一定的麻烦。例如,同样是上面的代码,如果我们输入的日期格式不正确,那么会引发异常:
请输入一个日期/时间: test 未经处理的异常: System.FormatException: 该字符串未被识别为有效的 DateTime。有一个未知单词(从索引 0 处开始)。 在 System.DateTimeParse.Parse(String s, DateTimeFormatInfo dtfi, DateTimeStyles styles) 在 lambda_method(Closure , String ) 在 ConsoleApplication1.Program.Main(String[] args) 位置 ConsoleApplication1\Program.cs:行号 25 请按任意键继续. . .
是的,根据这样的提示,我们可以判定是`lambda_method`的里面发生了异常,在这个案例中,由于代码较为简单,因此查错非常容易。然而,当你的表达式树非常复杂时,仅仅知道异常发生在`lambda_method`的内部是远远不够的。而根据现有的调试手段,是无法进入动态方法内部的。因此,只能使用一些间接的调试手段,例如插入测试函数调用:
using System; using System.Linq.Expressions; namespace ConsoleApplication1 { class Program { static void TestPoint(object v) { Console.WriteLine("Test Point : {0}", v); } static void Main(string[] args) { // 构造一个函数 DateTime f(string prompt); var argPrompt = Expression.Parameter(typeof(string), "propmpt"); // 局部变量。 var localInput = Expression.Parameter(typeof(string), "input"); var Console_WriteLine = typeof (Console).GetMethod("WriteLine", new[] {typeof (string)}); // 表达式块。 var expr = Expression.Block(typeof (DateTime), new[] {localInput}, Expression.Call(Console_WriteLine, argPrompt), Expression.Assign(localInput, Expression.Call(typeof (Console), "ReadLine", null)), Expression.Call(typeof (Program), "TestPoint", null, localInput), Expression.Call(typeof (DateTime), "Parse", null, localInput)); // 根据表达式块生成 Lambda 函数。 var lambda = Expression.Lambda(expr, argPrompt); // 编译 Lambda 函数,生成委托。 var f = (Func<string, DateTime>) lambda.Compile(); Console.WriteLine(f("请输入一个日期/时间:")); } } }
或者,我们也可以更为直接,例如调用`Debug.Print`。对于以上代码,如果接收到错误输入,那么应该会有如下输出:
请输入一个日期/时间: test Test Point : test 未经处理的异常: System.FormatException: 该字符串未被识别为有效的 DateTime。有一个未知单词(从索引 0 处开始)。 在 System.DateTimeParse.Parse(String s, DateTimeFormatInfo dtfi, DateTimeStyles styles) 在 lambda_method(Closure , String ) 在 ConsoleApplication1.Program.Main(String[] args) 位置 c:\Users\CXY\AppData\Local\Temporary Projects\ConsoleApplication1\Program.cs:行号 30 请按任意键继续. . .
实际上,如果我们在`TestPoint`函数中插入断点,运行到断点后,可以看到如下的调用堆栈:
> ConsoleApplication1.Program.TestPoint(object v) 行 10 C# [轻量函数] ConsoleApplication1.Program.Main(string[] args) 行 30 C# [本机到托管的转换] [托管到本机的转换] ...
这从另一个方面反映了动态生成的函数真的没法调试。
但`Linq.Expreesion`在设计时已经考虑到了调试的问题,所以引入了一个辅助属性。当你在DEBUG模式下时,可以在`Expression`实例中找到这个`DebugInfo`属性。它包含了这个`Expression`所包含的表达式的文本表示。(c.f. 调试表达式树)
- lambda {propmpt => {var input; ... }} System.Linq.Expressions.LambdaExpression {System.Linq.Expressions.Expression<System.Func<string,System.DateTime>>} + Body {var input; ... } System.Linq.Expressions.Expression {System.Linq.Expressions.ScopeN} CanReduce false bool DebugView ".Lambda #Lambda1<System.Func`2[System.String,System.DateTime]>(System.String $propmpt) {\r\n .Block(System.String $input) {\r\n .Call System.Console.WriteLine($propmpt);\r\n $input = .Call System.Console.ReadLine();\r\n .Call ConsoleApplication1.Program.TestPoint($input);\r\n .Call System.DateTime.Parse($input)\r\n }\r\n}" string Name null string NodeType Lambda System.Linq.Expressions.ExpressionType + Parameters Count = 1 System.Collections.ObjectModel.ReadOnlyCollection<System.Linq.Expressions.ParameterExpression> {System.Runtime.CompilerServices.TrueReadOnlyCollection<System.Linq.Expressions.ParameterExpression>} + ReturnType {Name = "DateTime" FullName = "System.DateTime"} System.Type {System.RuntimeType} TailCall false bool + Type {Name = "Func`2" FullName = "System.Func`2[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.DateTime, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]"} System.Type {System.RuntimeType} + 原始视图
使用文本可视化工具,可以看到`DebugView`属性的完整内容:
.Lambda #Lambda1<System.Func`2[System.String,System.DateTime]>(System.String $propmpt) { .Block(System.String $input) { .Call System.Console.WriteLine($propmpt); $input = .Call System.Console.ReadLine(); .Call ConsoleApplication1.Program.TestPoint($input); .Call System.DateTime.Parse($input) } }
关于`DebugView`属性中各个表达式的具体含义,可以参阅MSDN上的专题:调试表达式树。
最后,把我之前写的 XSerializer 的一个测试样例运行过程中生成的表达式树放上来……
.Lambda #Lambda1<System.Action`4[System.Xml.Linq.XElement,System.Object,Undefined.Serialization.XSerializationState,Undefined.Serialization.SerializationScope]>( System.Xml.Linq.XElement $element, System.Object $obj, Undefined.Serialization.XSerializationState $state, Undefined.Serialization.SerializationScope $typeScope) { .Block(UnitTestProject1.MyObject1 $localObj) { .Invoke (.Lambda #Lambda2<System.Action`1[System.Object]>)($obj); $localObj = (UnitTestProject1.MyObject1)$obj; .Call $element.SetElementValue( .Constant<System.Xml.Linq.XName>({http://www.yourcompany.org/schemas/undefined/1}property1), (System.Object)$localObj.Property1); .Call $element.Add(.Call $state.SerializeXProperty( (System.Object)$localObj.List1, .Constant<System.RuntimeType>(System.Collections.Generic.List`1[System.String]), .Constant<System.Xml.Linq.XName>({http://www.yourcompany.org/schemas/undefined/1}list1), .Constant<Undefined.Serialization.SerializationScope>(System.Collections.Generic.List`1[System.String] List1 (UnitTestProject1.MyObject1))) ); .Call $element.Add(.Call $state.SerializeXProperty( (System.Object)$localObj.Array1, .Constant<System.RuntimeType>(System.Object[]), .Constant<System.Xml.Linq.XName>({http://www.yourcompany.org/schemas/undefined/1}compositeArray), .Constant<Undefined.Serialization.SerializationScope>(System.Object[] Array1 (UnitTestProject1.MyObject1)))); .Call $element.Add(.Call $state.SerializeXProperty( (System.Object)$localObj.myObject, .Constant<System.RuntimeType>(System.Object), .Constant<System.Xml.Linq.XName>(myObject), null)); .Call $element.SetElementValue( .Constant<System.Xml.Linq.XName>({http://www.yourcompany.org/schemas/undefined/1}myDouble), (System.Object)$localObj.Field1); .Call $element.SetElementValue( .Constant<System.Xml.Linq.XName>({http://www.yourcompany.org/schemas/undefined/2}now), (System.Object)$localObj.Field2); .Call $element.SetElementValue( .Constant<System.Xml.Linq.XName>(Field3), (System.Object)$localObj.Field3); .Call $element.SetElementValue( .Constant<System.Xml.Linq.XName>(nullableInt), (System.Object)$localObj.Field4); .Call $element.SetAttributeValue( .Constant<System.Xml.Linq.XName>(myGuid), (System.Object)$localObj.Field5); .Call $element.Add(.Call $state.SerializeXProperty( (System.Object)$localObj.AnotherObject, .Constant<System.RuntimeType>(UnitTestProject1.MyObject1), .Constant<System.Xml.Linq.XName>({http://www.yourcompany.org/schemas/undefined/1}AnotherObject), null)) } } .Lambda #Lambda2<System.Action`1[System.Object]>(System.Object $obj) { .Call System.Diagnostics.Debug.Assert(.Call (.Constant<Undefined.Serialization.XSerializerBuilder+<>c__DisplayClassb>(Undefined.Serialization.XSerializerBuilder+<>c__DisplayClassb).t).IsInstanceOfType($obj) ) }