C# 反射與特性:EMIT 構(gòu)建代碼

本期我們將學(xué)習(xí) .NET Core 中,關(guān)于動態(tài)構(gòu)建代碼的知識。其中表達(dá)式樹已經(jīng)在另一個系列寫了,所以本系列主要是講述 反射,Emit ,AOP 等內(nèi)容。
如果現(xiàn)在總結(jié)一下,反射,與哪些數(shù)據(jù)結(jié)構(gòu)相關(guān)?
我們可以從 AttributeTargets 枚舉中窺見:

分別是程序集、模塊、類、結(jié)構(gòu)體、枚舉、構(gòu)造函數(shù)、方法、屬性、字段、事件、接口、參數(shù)、委托、返回值。
以往的文章中,已經(jīng)對這些進(jìn)行了很詳細(xì)的講解,我們可以中反射中獲得各種各樣的信息。當(dāng)然,我們也可以通過動態(tài)代碼,生成以上數(shù)據(jù)結(jié)構(gòu)。
動態(tài)代碼的其中一種方式是表達(dá)式樹,我們還可以使用 Emit 技術(shù)、Roslyn 技術(shù)來編寫;相關(guān)的框架有 Natasha、CS-Script 等。
01PART構(gòu)建代碼
首先我們引入一個命名空間:
using System.Reflection.Emit;
Emit 命名空間中里面有很多用于構(gòu)建動態(tài)代碼的類型,例如?AssemblyBuilder
,這個類型用于構(gòu)建程序集。類推,構(gòu)建其它數(shù)據(jù)結(jié)構(gòu)例如方法屬性,則有?MethodBuilder
、PropertyBuilder
?。
程序集(Assembly)
AssemblyBuilder 類型定義并表示動態(tài)程序集,它是一個密封類,其定義如下:
public sealed class AssemblyBuilder : Assembly
AssemblyBuilderAccess 定義動態(tài)程序集的訪問模式,在 .NET Core 中,只有兩個枚舉:

另外,程序集的構(gòu)建方式(API)也做了變更,如果你百度看到文章AppDomain.CurrentDomain.DefineDynamicAssembly
,那么你可以關(guān)閉創(chuàng)建了,說明里面的很多代碼根本無法在 .NET Core 下跑。
好了,不再贅述,我們來看看創(chuàng)建一個程序集的代碼:
AssemblyName assemblyName = new AssemblyName("MyTest"); AssemblyBuilder
assBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName,
AssemblyBuilderAccess.Run);
構(gòu)建程序集,分為兩部分:
AssemblyName 完整描述程序集的唯一標(biāo)識。
AssemblyBuilder 構(gòu)建程序集
一個完整的程序集,有很多信息的,版本、作者、構(gòu)建時間、Token 等,這些可以使用
AssemblyName 來設(shè)置。
一般一個程序集需要包含以下內(nèi)容:
簡單名稱。
版本號。
加密密鑰對。
支持的區(qū)域性。
你可以參考以下示例:

最終程序集的 AssemblyName 顯示名稱是以下格式的字符串:
Name <,culture cultureinfo=""> <,Version = Major.Minor.Build.Revi
sion> <, strongname=""> <,publickeytoken> '\0'
例如:
ExampleAssembly, Version=1.0.0.0, Culture=en, PublicKeyToken=a5d
015c7d5a0b012
另外,創(chuàng)建程序集構(gòu)建器使用?AssemblyBuilder.DefineDynamicAssembly()
?而不是?new AssemblyBuilder()
?。
模塊(Module)
程序集和模塊之間的區(qū)別可以參考
模塊是程序集內(nèi)代碼的邏輯集合,每個模塊可以使用不同的語言編寫,大多數(shù)情況下,一個程序集包含一個模塊。程序集包括了代碼、版本信息、元數(shù)據(jù)等。
MSDN指出:“模塊是沒有 Assembly 清單的 Microsoft 中間語言(MSIL)文件?!薄?/p>
這些就不再扯淡了。
創(chuàng)建完程序集后,我們繼續(xù)來創(chuàng)建模塊。

類型(Type)
目前步驟:
Assembly -> Module -> Type 或 Enum
ModuleBuilder 中有個?DefineType
?方法用于創(chuàng)建?class
?和?struct
;DefineEnum
方法用于創(chuàng)建?enum
。
這里我們分別說明。
創(chuàng)建類或結(jié)構(gòu)體:
TypeBuilder typeBuilder = moduleBuilder.DefineType("MyTest.MyCla
ss",TypeAttributes.Public);
定義的時候,注意名稱是完整的路徑名稱,即命名空間+類型名稱。
我們可以先通過反射,獲取已經(jīng)構(gòu)建的代碼信息:

結(jié)果:

接下來將創(chuàng)建一個枚舉類型,并且生成枚舉。
我們要創(chuàng)建一個這樣的枚舉:

namespace MyTest { public enum MyEnum { Top = 1, Bottom = 2, Lef
t = 4, Right = 8, All = 16 } }
使用 Emit 的創(chuàng)建過程如下:
EnumBuilder enumBuilder = moduleBuilder.DefineEnum("MyTest.MyEn
um", TypeAttributes.Public, typeof(int));
TypeAttributes 有很多枚舉,這里只需要知道聲明這個枚舉類型為 公開的(Public);typeof(int)
?是設(shè)置枚舉數(shù)值基礎(chǔ)類型。
然后 EnumBuilder 使用?DefineLiteral
?方法來創(chuàng)建枚舉。
代碼如下:

我們可以使用反射將創(chuàng)建的枚舉打印出來:

Main 方法中調(diào)用:
WriteEnum(enumBuilder.CreateTypeInfo());
接下來,類型創(chuàng)建成員,就復(fù)雜得多了。
DynamicMethod定義方法與添加IL
下面我們來為 類型創(chuàng)建一個方法,并通過 Emit 向程序集中動態(tài)添加 IL。這里并不是使用 MethodBuider,而是使用 DynamicMethod。
在開始之前,請自行安裝反編譯工具 dnSpy 或者其它工具,因?yàn)檫@里涉及到 IL 代碼。
這里我們先忽略前面編寫的代碼,清空 Main 方法。
我們創(chuàng)建一個類型:
public class MyClass{}
這個類型什么都沒有。
然后使用 Emit 動態(tài)創(chuàng)建一個 方法,并且附加到 MyClass 類型中:

運(yùn)行后會打印字符串。
DynamicMethod 類型用于構(gòu)建方法,定義并表示可以編譯、執(zhí)行和丟棄的一種動態(tài)方法。 丟棄的方法可用于垃圾回收。。
ILGenerator 是 IL 代碼生成器。
EmitWriteLine 作用是打印字符串,
OpCodes.Ret 標(biāo)記 結(jié)束方法的執(zhí)行,
Invoke 將方法轉(zhuǎn)為委托執(zhí)行。
上面的示例比較簡單,請認(rèn)真記一下。
下面,我們要使用 Emit 生成一個這樣的方法:

看起來很簡單的代碼,要用 IL 來寫,就變得復(fù)雜了。
ILGenerator 正是使用 C# 代碼的形式去寫 IL,但是所有過程都必須按照 IL 的步驟去寫。
其中最重要的,便是 OpCodes 枚舉了,OpCodes 有幾十個枚舉,代表了 IL 的所有操作功能。
如果你點(diǎn)擊上面的鏈接查看 OpCodes 的枚舉,你可以看到,很多 功能碼,這么多功能碼是記不住的。我們現(xiàn)在剛開始學(xué)習(xí) Emit,這樣就會難上加難。
所以,我們要先下載能夠查看 IL 代碼的工具,方便我們探索和調(diào)整寫法。
我們看看此方法生成的 IL 代碼:

看不懂完全沒關(guān)系,因?yàn)楣P者也看不懂。
目前我們已經(jīng)獲得了上面兩大部分的信息,接下來我們使用?DynamicMethod
?來動態(tài)編寫方法。
定義 Add 方法并獲取 IL 生成工具:

DynamicMethod 用于定義一個方法;ILGenerator是 IL 生成器。當(dāng)然也可以將此方法附加到一個類型中,完整代碼示例如下:

實(shí)際以上代碼與我們反編譯出來的 IL 編寫有所差異,具體俺也不知道為啥,在群里問了調(diào)試了,注釋掉那么幾行代碼,才通過的。