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