程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> 關於.NET >> Emit學習-基礎篇-基本概念介紹

Emit學習-基礎篇-基本概念介紹

編輯:關於.NET

之前的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來替代掉這兩個方法,將反射 帶來的性能影響降到最低,這個將在以後講到。

本文配套源碼

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved