.NET高級特性-Emit(2)類的定義
- 2020 年 2 月 12 日
- 筆記
在上一篇博文發了一天左右的時間,就收到了部落格園許多讀者的評論和推薦,非常感謝,我也會及時回復讀者的評論。之後我也將繼續撰寫博文,梳理相關.NET的知識,希望.NET的圈子能越來越大,開發者能了解/深入.NET的本質,將工作做的簡單又高效,拒絕重複勞動,拒絕CRUD。
ok,咱們開始繼續Emit的探索。在這之前,我先放一下我往期關於Emit的文章,方便讀者閱讀。
《.NET高級特性-Emit(1)》
一、基礎知識
既然C#作為一門面向對象的語言,所以首當其衝的我們需要讓Emit為我們動態構建類。
廢話不多說,首先,我們先來回顧一下C#類的內部由什麼東西組成:
(1) 欄位-C#類中保存數據的地方,由訪問修飾符、類型和名稱組成;
(2) 屬性-C#類中特有的東西,由訪問修飾符、類型、名稱和get/set訪問器組成,屬性的是用來控制類中欄位數據的訪問,以實現類的封裝性;在Java當中寫作getXXX()和setXXX(val),C#當中將其變成了屬性這種語法糖;
(3) 方法-C#類中對邏輯進行操作的基本單元,由訪問修飾符、方法名、泛型參數、入參、出參構成;
(4) 構造器-C#類中一種特殊的方法,該方法是專門用來創建對象的方法,由訪問修飾符、與類名相同的方法名、入參構成。
接著,我們再觀察C#類本身又具備哪些東西:
(1) 訪問修飾符-實現對C#類的訪問控制
(2) 繼承-C#類可以繼承一個父類,並需要實現父類當中所有抽象的方法以及選擇實現父類的虛方法,還有就是子類需要調用父類的構造器以實現對象的創建
(3) 實現-C#類可以實現多個介面,並實現介面中的所有方法
(4) 泛型-C#類可以包含泛型參數,此外,類還可以對泛型實現約束
以上就是C#類所具備的一些元素,以下為樣例:
public abstract class Bar { public abstract void PrintName(); } public interface IFoo<T> { public T Name { get; set; } } //繼承Bar基類,實現IFoo介面,泛型參數T public class Foo<T> : Bar, IFoo<T> //泛型約束 where T : struct { //構造器 public Foo(T name):base() { _name = name; } //欄位 private T _name; //屬性 public T Name { get => _name; set => _name = value; } //方法 public override void PrintName() { Console.WriteLine(_name.ToString()); } }
在探索完了C#類及其定義後,我們要來了解C#的項目結構組成。我們知道C#的一個csproj項目最終會對應生成一個dll文件或者exe文件,這一個文件我們稱之為程式集Assembly;而在一個程式集中,我們內部包含和定義了許多命名空間,這些命令空間在C#當中被稱為模組Module,而模組正是由一個一個的C#類Type組成。

所以,當我們需要定義C#類時,就必須首先定義Assembly以及Module,如此才能進行下一步工作。
二、IL概覽
由於Emit實質是通過IL來生成C#程式碼,故我們可以反向生成,先將寫好的目標程式碼寫成cs文件,通過編譯器生成dll,再通過ildasm查看IL程式碼,即可依葫蘆畫瓢的編寫出Emit程式碼。所以我們來查看以下上節Foo所生成的IL程式碼。

從上圖我們可以很清晰的看到.NET的層級結構,位於樹頂層淺藍色圓點表示一個程式集Assembly,第二層藍色表示模組Module,在模組下的均為我們所定義的類,類中包含類的泛型參數、繼承類資訊、實現介面資訊,類的內部包含構造器、方法、欄位、屬性以及它的get/set方法,由此,我們可以開始編寫Emit程式碼了
三、Emit編寫
有了以上的對C#類的解讀和IL的解讀,我們知道了C#類本身所需要哪些元素,我們就開始根據這些元素來開始編寫Emit程式碼了。這裡的程式碼量會比較大,請讀者慢慢閱讀,也可以參照以上我寫的類生成il程式碼進行比對。
在Emit當中所有創建類型的幫助類均以Builder結尾,從下表中我們可以看的非常清楚
元素中文 |
元素名稱 |
對應Emit構建器名稱 |
---|---|---|
程式集 |
Assembly |
AssemblyBuilder |
模組 |
Module |
ModuleBuilder |
類 |
Type |
TypeBuilder |
構造器 |
Constructor |
ConstructorBuilder |
屬性 |
Property |
PropertyBuilder |
欄位 |
Field |
FieldBuilder |
方法 |
Method |
MethodBuilder |
由於創建類需要從Assembly開始創建,所以我們的入口是AssemblyBuilder
(1) 首先,我們先引入命名空間,我們以上節Foo類為樣例進行編寫
using System.Reflection.Emit;
(2) 獲取基類和介面的類型
var barType = typeof(Bar); var interfaceType = typeof(IFoo<>);
(3) 定義Foo類型,我們可以看到在定義類之前我們需要創建Assembly和Module
//定義類 var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName("Edwin.Blog.Emit"), AssemblyBuilderAccess.Run); var moduleBuilder = assemblyBuilder.DefineDynamicModule("Edwin.Blog.Emit"); var typeBuilder = moduleBuilder.DefineType("Foo", TypeAttributes.Public | TypeAttributes.Class | TypeAttributes.AutoClass | TypeAttributes.AnsiClass | TypeAttributes.BeforeFieldInit);
(4) 定義泛型參數T,並添加約束
//定義泛型參數 var genericTypeBuilder = typeBuilder.DefineGenericParameters("T")[0]; //設置泛型約束 genericTypeBuilder.SetGenericParameterAttributes(GenericParameterAttributes.NotNullableValueTypeConstraint);
(5) 繼承和實現介面,注意當實現類的泛型參數需傳遞給介面時,需要將泛型介面添加泛型參數後再調用AddInterfaceImplementation方法
//繼承基類 typeBuilder.SetParent(barType); //實現介面 typeBuilder.AddInterfaceImplementation(interfaceType.MakeGenericType(genericTypeBuilder));
(6) 定義欄位,因為欄位在構造器值需要使用,故先創建
//定義欄位 var fieldBuilder = typeBuilder.DefineField("_name", genericTypeBuilder, FieldAttributes.Private);
(7) 定義構造器,並編寫內部邏輯
//定義構造器 var ctorBuilder = typeBuilder.DefineConstructor(MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName, CallingConventions.Standard, new Type[] { genericTypeBuilder }); var ctorIL = ctorBuilder.GetILGenerator(); //Ldarg_0在實例方法中表示this,在靜態方法中表示第一個參數 ctorIL.Emit(OpCodes.Ldarg_0); ctorIL.Emit(OpCodes.Ldarg_1); //為field賦值 ctorIL.Emit(OpCodes.Stfld, fieldBuilder); ctorIL.Emit(OpCodes.Ret);
(8) 定義Name屬性
//定義屬性 var propertyBuilder = typeBuilder.DefineProperty("Name", PropertyAttributes.None, genericTypeBuilder, Type.EmptyTypes);
(9) 編寫Name屬性的get/set訪問器
//定義get方法 var getMethodBuilder = typeBuilder.DefineMethod("get_Name", MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.NewSlot | MethodAttributes.SpecialName | MethodAttributes.Virtual, CallingConventions.Standard, genericTypeBuilder, Type.EmptyTypes); var getIL = getMethodBuilder.GetILGenerator(); getIL.Emit(OpCodes.Ldarg_0); getIL.Emit(OpCodes.Ldfld, fieldBuilder); getIL.Emit(OpCodes.Ret); typeBuilder.DefineMethodOverride(getMethodBuilder, interfaceType.GetProperty("Name").GetGetMethod()); //實現對介面方法的重載 propertyBuilder.SetGetMethod(getMethodBuilder); //設置為屬性的get方法 //定義set方法 var setMethodBuilder = typeBuilder.DefineMethod("set_Name", MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.NewSlot | MethodAttributes.SpecialName | MethodAttributes.Virtual, CallingConventions.Standard, null, new Type[] { genericTypeBuilder }); var setIL = setMethodBuilder.GetILGenerator(); setIL.Emit(OpCodes.Ldarg_0); setIL.Emit(OpCodes.Ldarg_1); setIL.Emit(OpCodes.Stfld, fieldBuilder); setIL.Emit(OpCodes.Ret); typeBuilder.DefineMethodOverride(setMethodBuilder, interfaceType.GetProperty("Name").GetSetMethod()); //實現對介面方法的重載 propertyBuilder.SetSetMethod(setMethodBuilder); //設置為屬性的set方法
(10) 定義並實現PrintName方法
//定義方法 var printMethodBuilder = typeBuilder.DefineMethod("PrintName", MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.Virtual, CallingConventions.Standard, null, Type.EmptyTypes); var printIL = printMethodBuilder.GetILGenerator(); printIL.Emit(OpCodes.Ldarg_0); printIL.Emit(OpCodes.Ldflda, fieldBuilder); printIL.Emit(OpCodes.Constrained, genericTypeBuilder); printIL.Emit(OpCodes.Callvirt, typeof(object).GetMethod("ToString", Type.EmptyTypes)); printIL.Emit(OpCodes.Call, typeof(Console).GetMethod("WriteLine", new Type[] { typeof(string) })); printIL.Emit(OpCodes.Ret); //實現對基類方法的重載 typeBuilder.DefineMethodOverride(printMethodBuilder, barType.GetMethod("PrintName", Type.EmptyTypes));
(11) 創建類
var type = typeBuilder.CreateType(); //netstandard中請使用CreateTypeInfo().AsType()
(12) 調用
var obj = Activator.CreateInstance(type.MakeGenericType(typeof(DateTime)), DateTime.Now); (obj as Bar).PrintName(); Console.WriteLine((obj as IFoo<DateTime>).Name);
四、應用
上面的樣例僅供學習只用,無法運用在實際項目當中,那麼,Emit構建類在實際項目中我們可以有什麼應用,提高我們的編碼效率
(1) 動態DTO-當我們需要將實體映射到某個DTO時,可以用動態DTO來代替你手寫的DTO,選擇你需要的欄位回傳給前端,或者前端把他想要的欄位傳給後端
(2) DynamicLinq-我的第一篇博文有個讀者提到了表達式樹,而linq使用的正是表達式樹,當表達式樹+Emit時,我們就可以用像SQL或者GraphQL那樣的查詢語句實現動態查詢
(3) 對象合併-我們可以編寫實現一個像js當中Object.assign()一樣的方法,實現對兩個實體的合併
(4) AOP動態代理-AOP的核心就是代理模式,但是與其對應的是需要手寫代理類,而Emit就可以幫你動態創建代理類,實現切面編程
(5) …
五、小結
對於Emit,確實初學者會對其感到複雜和難以學習,但是只要搞懂其中的原理,其實最終就是C#和.NET語言的本質所在,在學習Emit的同時,也是在鍛煉你的基本功是否紮實,你是否對這門語言精通,是否有各種簡化程式碼的應用。
保持學習,勇於實踐;Write Less,Do More;作者之後還會繼續.NET高級特性系列,感謝閱讀!