.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高級特性系列,感謝閱讀!