關於 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) 位置 ConsoleApplication1Program.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:UsersCXYAppDataLocalTemporary ProjectsConsoleApplication1Program.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) {rn    .Block(System.String $input) {rn        .Call System.Console.WriteLine($propmpt);rn        $input = .Call System.Console.ReadLine();rn        .Call ConsoleApplication1.Program.TestPoint($input);rn        .Call System.DateTime.Parse($input)rn    }rn}"	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 服務減少垃圾留言。進一步了解 Akismet 如何處理網站訪客的留言資料

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