之前的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#代碼的實現,然后再以這為目標進行動態構建:


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代碼是什么樣的,結果如下:


.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? 從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來替代掉這兩個方法,將反射帶來的性能影響降到最低,這個將在以后講到。最后在這里提供源程序的下載?Fibonacci
?
?