在上一篇博文發了一天左右的時間,就收到了博客園許多讀者的評論和推薦,非常感謝,我也會及時回復讀者的評論。之后我也將繼續撰寫博文,梳理相關.NET的知識,希望.NET的圈子能越來越大,開發者能了解/深入.NET的本質,將工作做的簡單又高效,拒絕重復勞動,拒絕CRUD。
ok,咱們開始繼續Emit的探索。在這之前,我先放一下我往期關於Emit的文章,方便讀者閱讀。
一、基礎知識
既然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高級特性系列,感謝閱讀!