关于 Linq.Expression 的一些调试技巧

使用 `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)
    )
}

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

ERROR: si-captcha.php plugin: GD image support not detected in PHP!

Contact your web host and ask them to enable GD image support for PHP.

ERROR: si-captcha.php plugin: imagepng function not detected in PHP!

Contact your web host and ask them to enable imagepng for PHP.

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据

Content is available under CC BY-SA 3.0 unless otherwise noted.