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