.NET高級特性-Emit

  • 2020 年 2 月 12 日
  • 筆記

前言

在這個大數據/雲計算/人工智慧研發普及的時代,Python的崛起以及Javascript的前後端的侵略,程式設計師與企業似乎越來越青睞動態語言所帶來的便捷性與高效性,即使靜態語言在性能,錯誤檢查等方面的優於靜態語言。

.NET做為一門靜態語言,我們不僅要打好.NET的基本功,如基本類型/語法/底層原理/錯誤檢查等知識,也要深入理解.NET的一些高級特性,來為你的工作減輕負擔和提高程式碼品質。

一、什麼是Emit?

Emit含義為發出、產生的含義,這是.NET中的一組類庫,命名空間為System.Reflection.Emit,幾乎所有的.NET版本(Framework/Mono/NetCore)都支援Emit,可以實現用C#程式碼生成程式碼的類庫

二、Emit的本質

我們知道.NET可以由各種語言進行編寫,比如VB,C++等,當然絕大部分程式設計師進行.NET開發都是使用C#語言進行的,這些語言都會被各自的語言解釋器解釋為IL語言並執行,而Emit類庫的作用就是用這些語言來編寫生成IL語言,並交給CLR(公共語言運行時)進行執行。

我們先來看看IL語言長什麼樣子:

1、首先我們創建一個Hello,World程式

class Program  {      static void Main(string[] args)      {          Console.WriteLine("Hello World!");      }  }

2、將程式編譯成dll文件,我們可以看到在開發目錄下生成了bin文件夾

3、向下尋找,我們可以看到dll文件已經生成,筆者使用.NET Core 3 進行開發,故路徑為bin/Debug/netcoreapp3.0

4、這時候,我們就要祭出我們的il查看神器了,ildasm工具

如何找到這個工具?打開開始菜單,找到Visual Studio文件夾,打開Developer Command Prompt,在打開的命令行中鍵入ildasm回車即可,筆者使用vs2019進行演示,其它vs版本操作方法均一致

5、在dasm菜單欄選擇文件->打開,選擇剛剛生成的dll文件

6、即可查看生成il程式碼

有了ildasm的輔助,我們就能夠更好的了解IL語言以及如何編寫IL語言,此外,Visual Studio中還有許多插件支援查看il程式碼,比如JetBrains出品的Resharper插件等,如果覺得筆者方式較為麻煩可以使用以上插件查看il程式碼

三、理解IL程式碼

我們理解了Emit的本質其實就是用C#來編寫IL程式碼,既然要編寫IL程式碼,那麼我們首先要理解IL程式碼是如何進行工作的,IL程式碼是如何完成C#當中的順序/選擇/循環結構的,是如何實現類的定義/欄位的定義/屬性的定義/方法的定義的。

IL程式碼是一種近似於指令式的程式碼語言,與彙編語言比較相近,所以習慣於寫高級語言的.NETer來說比較難以理解

讓我們來看看Hello,World程式的IL程式碼:

IL_0000:  nop  IL_0001:  ldstr      "Hello World!"  IL_0006:  call       void [System.Console]System.Console::WriteLine(string)  IL_000b:  nop  IL_000c:  ret

我們可以把IL程式碼看成棧的運行

第一條指令,nop表示不做任何事情,表示程式碼不做任何事情

第二條指令,ldstr表示將字元串放入棧中,字元串的值為「Hello,World!」

第三條指令,call表示調用方法,參數為調用方法的方法資訊,並把返回的結構壓入棧中,使用的參數為之前已經入棧的「Hello World!」,以此類推,如果方法有n個參數,那麼他就會調取棧中n個數據,並返回一個結果放回棧中

第四條指令,nop表示不做任何事情

第五條指令,ret表示將棧中頂部的數據返回,如果方法定義為void,則無返回值

關於Hello,world程式IL的理解就說到這裡,更多的指令含義讀者可以參考微軟官方文檔,筆者之後也會繼續對Emit進行講解和Emit的應用

四、用Emit類庫編寫IL程式碼

既然IL程式碼咱們理解的差不多了,咱們就開始嘗試用C#來寫IL程式碼了,有了IL程式碼的參考,咱們也可以依葫蘆畫瓢的把程式碼寫出來了

1、引入Emit命名空間

using System.Reflection.Emit;

2、首先我們定義一個Main方法,入參無,返回類型void

//定義方法名,返回類型,輸入類型  var method = new DynamicMethod("Main", null, Type.EmptyTypes);

3、生成IL程式碼

//生成IL程式碼  var ilGenerator = method.GetILGenerator();  ilGenerator.Emit(OpCodes.Nop);  ilGenerator.Emit(OpCodes.Ldstr,"Hello World!");  ilGenerator.Emit(OpCodes.Call, typeof(Console).GetMethod("WriteLine", new Type[] { typeof(string) })); //尋找Console的WriteLine方法  ilGenerator.Emit(OpCodes.Nop);  ilGenerator.Emit(OpCodes.Ret);

4、創建委託並調用

//創建委託  var helloWorldMethod = method.CreateDelegate(typeof(Action)) as Action;  helloWorldMethod.Invoke();

5、運行,即輸出Hello World!

五、小結

Emit的本質是使用高級語言生成IL程式碼,進而進行調用的的一組類庫,依賴Emit我們可以實現用程式碼生成程式碼的操作,即程式語言的自舉,可以有效彌補靜態語言的靈活性的缺失。

Emit的性能非常好,除了第一次構建IL程式碼所需要時間外,之後只要將操作快取在電腦記憶體中,速度與手寫程式碼相差無幾

有許多著名.NET類庫均依賴於Emit:

  • (.NET JSON操作庫)Json.NET/Newtonsoft.Json: https://github.com/JamesNK/Newtonsoft.Json
  • (輕量ORM)Dapper:https://github.com/StackExchange/Dapper
  • (ObjectToObjectMapper)EmitMapper:https://github.com/MetSystem/EmitMapper
  • (AOP庫)Castle.DynamicProxy:https://github.com/castleproject/Core

學習Emit

.NET官方文檔:https://docs.microsoft.com/zh-cn/dotnet

.NET API瀏覽器:https://docs.microsoft.com/zh-cn/dotnet/api