之前的Hello World例子應該已經讓我們對Emit有了一個模糊的了解,那麼 Emit到底是什麼樣一個東西,他又能實現些什麼功能呢?昨天查了點資料,大致 總結了下,由於才開始學習肯定有不完善的地方,希望大家能夠批評指正。
1.什麼是反射發出(Reflection Emit)
Emit應該是屬於反射中的一個比較高級的功能,說到反射大家應該都不陌生, 反射是在運行時發現對象的相關信息,並且執行這些對象(創建對象實例,執行 對象上的方法)。這個功能是由.NET的System.Reflection命名空間的類所提供的 。簡單的說,它們不僅允許你浏覽一個程序集暴露的類、方法、屬性和字段,而 且還允許你創建一個類型的實例以及執行這些類型上的方法(調用成員)。這些 特性對於在運行時對象發現,已經很了不起了,但.NET的反射機制並沒有到此結 束。反射還允許你在運行時構建一個程序集,並且可以創建全新的類型。這就是 反射發出(reflection emit)。
使用Emit可以從零開始,動態的構造程序集和類型,在需要時動態的生成代碼 ,提高程序的靈活性。有了這些功能,我們可以用其來實現一些典型的應用,如 :
l 動態代理(AOP);
l 減少反射的性能損失(Dynamic Method等);
l ORM的實現;
l 工具及IDE插件的開發;
l 公共代碼安全模塊的開發。
2.使用Emit的完整流程
使用Emit一般包括以下步驟:
1)創建一個新的程序集(可以選擇存在與內存中或者持久化到硬盤);
2)在程序集內創建一個模塊;
3)在模塊內創建動態類;
4)給動態類添加動態方法、屬性、事件,等;
5)生成相關的IL代碼;
6)返回創建出來的類型或持久化到硬盤中。
當然如果你只是想要創建一個Dynamic Method 那麼可以直接使用之前 HelloWorld例子中使用的DynamicMethod類來創建一個動態方法,並在構造函數時 傳入它所依附的類或者模塊。看了這個流程,相信大家已經對用使用Emit來創建 動態類型的過程有了一個直觀的認識,下面我們就通過實現一個求斐波那契數列 的類來加深對這一流程的了解。
在開始我們的例子之前,先給大家介紹一款反編譯軟件Reflector,使用這個 軟件可以給我們編寫IL代碼提供很大的幫助。
接下來我們按照上面所說的流程來創建我們的斐波那契類:
第一步:構建程序集
要構建一個動態的程序集,我們需要創建一個AssemblyBuilder對象, AssemblyBuilder類是整個反射發出工作的基礎,它為我們提供了動態構造程序集 的入口。要創建一個AssemblyBuilder對象,需要使用AppDomain的 DefineDynamicAssembly方法,該方法包括兩個最基本的參數:AssemblyName和 AssemblyBuilderAccess前者用來唯一標識一個程序集,後者用來表示動態程序集 的訪問方式,有如下的成員:
成員名稱 說明 Run 表示可以執行但不能保存此動態程序集。 Save 表示可以保存但不能執行此動態程序集。 RunAndSave 表示可以執行並保存此動態程序集。 ReflectionOnly 表示在只反射上下文中加載動態程序集,且不能執行此程序集。
在這裡我們選擇使用RunAndSave,完整的代碼如下:
#region Step 1 構建程序集
//創建程序集名 AssemblyName asmName = new AssemblyName ("EmitExamples.DynamicFibonacci"); //獲取程序集所在的應用程序域 //你也可以選擇用AppDomain.CreateDomain方法創建一個新的應用程序域 //這裡選擇當前的應用程序域 AppDomain domain = AppDomain.CurrentDomain; //實例化一個AssemblyBuilder對象來實現動態程序集的構建 AssemblyBuilder assemblyBuilder = domain.DefineDynamicAssembly(asmName, AssemblyBuilderAccess.RunAndSave); #endregion
第二步:定義模塊(Module)
與第一步類似,要定一個動態模塊,我們需要創建一個ModuleBuilder對象, 通過AssemblyBuilder對象的DefineDynamicModule方法,需要傳入模塊的名字( 如果要持久化到硬盤,那麼還需要傳入要保存的文件的名字,這裡就是我們的程 序集名),這裡我們使用程序集名作為模塊名字:
#region Step 2 定義模塊
ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule (name, asmFileName);
第三部:創建一個動態類型
這個時候恐怕我不說你也已經知道了,對,現在我們就是要用ModuleBuilder 來創建一個TypeBuilder的對象,如下:
#region Step 3 定義類型
TypeBuilder typeBuilder = moduleBuilder.DefineType ("EmitExamples.DynamicFibonacci", TypeAttributes.Public); #endregion
這裡EmitExamples表示名字空間,DynamicFibonacci是類的名字, TypeAttributes表示類的屬性,可以按照實際需要進行組合。
第四步:定義方法
到這裡為止,我們的准備工作已經差不多了,下面要開始真正的大展拳腳啦!
我們先來看一下我們接下來要實現的動態類C#代碼的實現,然後再以這為目標 進行動態構建:
Fibonacci public class Fibonacci { public int Calc(int num) { if (num == 1 || num == 2) { return 1; } else { return Calc(num - 1) + Calc(num - 2); } } }
OK,從上面的代碼可以看出我們需要創建一個名為Calc的Public方法,它具有 一個Int32型的傳入參數和返回值。同樣的,我們使用TypeBuilder的 DefineMethod方法來創建這樣一個MethodBuilder,如下:
#region Step 4 定義方法
MethodBuilder methodBuilder = typeBuilder.DefineMethod( "Calc", MethodAttributes.Public, typeof(Int32), new Type[] { typeof(Int32) }); #endregion
DefineMethod方法的四個參數分別是函數名,修飾符,返回值類型,傳入參數 的類型數組。
第五步:實現方法
現在就要為之前創建的Calc方法添加對應的IL代碼了,這對我們這些新手來說 這就顯的有點無從入手來了,不過沒關系,還記得我之前提到的那個反編譯工具 嗎?現在就是它發揮作用的時候了,我們用它來反編譯之前寫的Fibonacci類,看 看自動生成的IL代碼是什麼樣的,結果如下:
IL .method public hidebysig instance int32 Calc(int32 num) cil managed { .maxstack 4 .locals init ( [0] int32 CS$1$0000, [1] bool CS$4$0001) L_0000: nop L_0001: ldarg.1 L_0002: ldc.i4.1 L_0003: beq.s L_000e L_0005: ldarg.1 L_0006: ldc.i4.2 L_0007: ceq L_0009: ldc.i4.0 L_000a: ceq L_000c: br.s L_000f L_000e: ldc.i4.0 L_000f: stloc.1 L_0010: ldloc.1 L_0011: brtrue.s L_0018 L_0013: nop L_0014: ldc.i4.1 L_0015: stloc.0 L_0016: br.s L_002f L_0018: nop L_0019: ldarg.0 L_001a: ldarg.1 L_001b: ldc.i4.1 L_001c: sub L_001d: call instance int32 EmitExamples.Fibonacci::Calc(int32) L_0022: ldarg.0 L_0023: ldarg.1 L_0024: ldc.i4.2 L_0025: sub L_0026: call instance int32 EmitExamples.Fibonacci::Calc(int32) L_002b: add L_002c: stloc.0 L_002d: br.s L_002f L_002f: ldloc.0 L_0030: ret }
我們來對上面的IL代碼進行分析:
l 從L_0000到L_0003是加載參數一、加載整數1,然後判斷兩者是否相等, 如果相等則跳轉到L_000e繼續執行;
l 從L_0005到L_000e是加載參數一、加載整數2,然後判斷兩者是否相等, 如果相等則將整數1送到堆棧上,否則將整數0送到堆棧上;然後再加載整數0,用 之前比較的結果和0進行比較,如果相等則將整數1送到堆棧上,否則將整數0送到 堆棧上;這個時侯,如果傳入的參數是2那麼現在堆棧上的數字就是兩個0,兩者 相等,那麼跳轉到L_000f繼續執行,反之就繼續執行,加載數字0到堆棧上(是不 是感覺很復雜,沒關系,我們一會對其進行優化);
從L_000f到L_0016是判斷之前判斷的返回值,也就是說如果傳入的參數是1或 者2,那麼就將局部變量0的值設為1,然後跳轉到L_002f執行;反之就從L_0018開 始執行;
l 從L_0018到L_002b是把參數0和參數1加載(注意:在非靜態方法中,參數0 表示其對自身所在類的示例的引用,相當於this),然後將參數1分別減去1和2後 進行遞歸調用,並將結果相加,並把記過放到局部變量0中;
l 從L_002d到L_0030是加載局部變量0,並將結果返回。
有了之前分析的基礎,我們可以將流程簡化為如下步驟:
1) 如果傳入的參數是1,跳轉到第六步執行;
2) 如果傳入的參數是2,跳轉到第六步執行;
3) 將傳入的參數減1,然後遞歸調用自身;
4) 將傳入的參數減2,然後遞歸調用自身;
5) 將遞歸調用的結果相加,跳轉到第七步執行;
6) 設置堆棧頂的值為1;
7) 返回堆棧頂的元素作為結果。
然後我們就可以參照以上的反編譯出來的IL代碼,用Emit書寫出對應的IL代碼 ,具體代碼如下:
#region Step 5 實現方法
ILGenerator calcIL = methodBuilder.GetILGenerator();
//定義標簽lbReturn1,用來設置返回值為1
Label lbReturn1 = calcIL.DefineLabel();
//定義標簽lbReturnResutl,用來返回最終結果
Label lbReturnResutl = calcIL.DefineLabel();
//加載參數1,和整數1,相比較,如果相等則設置返回值為1
calcIL.Emit(OpCodes.Ldarg_1);
calcIL.Emit(OpCodes.Ldc_I4_1);
calcIL.Emit(OpCodes.Beq_S, lbReturn1);
//加載參數1,和整數2,相比較,如果相等則設置返回值為1
calcIL.Emit(OpCodes.Ldarg_1);
calcIL.Emit(OpCodes.Ldc_I4_2);
calcIL.Emit(OpCodes.Beq_S, lbReturn1);
//加載參數0和1,將參數1減去1,遞歸調用自身
calcIL.Emit(OpCodes.Ldarg_0);
calcIL.Emit(OpCodes.Ldarg_1);
calcIL.Emit(OpCodes.Ldc_I4_1);
calcIL.Emit(OpCodes.Sub);
calcIL.Emit(OpCodes.Call, methodBuilder);
//加載參數0和1,將參數1減去2,遞歸調用自身
calcIL.Emit(OpCodes.Ldarg_0);
calcIL.Emit(OpCodes.Ldarg_1);
calcIL.Emit(OpCodes.Ldc_I4_2);
calcIL.Emit(OpCodes.Sub);
calcIL.Emit(OpCodes.Call, methodBuilder);
//將遞歸調用的結果相加,並返回
calcIL.Emit(OpCodes.Add);
calcIL.Emit(OpCodes.Br, lbReturnResutl);
//在這裡創建標簽lbReturn1
calcIL.MarkLabel(lbReturn1);
calcIL.Emit(OpCodes.Ldc_I4_1);
//在這裡創建標簽lbReturnResutl
calcIL.MarkLabel(lbReturnResutl);
calcIL.Emit(OpCodes.Ret);
#endregion
第六步:創建類型,並持久化到硬盤
到上一步為止,我們已經完成了斐波那契類以及方法的完整創建,接下來就是 收獲的時候了,我們使用TypeBuilder的CreateType方法完成最終的創建過程;最 後使用AssemblyBuilder類的Save方法將程序集持久化到硬盤中,代碼如下:
#region Step 6 收獲
Type type = typeBuilder.CreateType();
assemblyBuilder.Save(asmFileName);
object ob = Activator.CreateInstance(type);
for (int i = 1; i < 10; i++)
{
Console.WriteLine(type.GetMethod("Calc").Invoke(ob, new object[] { i }));
}
#endregion
這裡使用Activator.CreateInstance方法創建了動態類型的一個實例,然後使 用MethodInfo的Invoke方法調用裡裡面的Calc方法,看起來需要通過多次反射, 好像性能並不是很好,但其實我們完全可以用Emit來替代掉這兩個方法,將反射 帶來的性能影響降到最低,這個將在以後講到。
本文配套源碼