1. 程式人生 > >.NET高階特性-Emit(2)類的定義

.NET高階特性-Emit(2)類的定義

  在上一篇博文發了一天左右的時間,就收到了部落格園許多讀者的評論和推薦,非常感謝,我也會及時回覆讀者的評論。之後我也將繼續撰寫博文,梳理相關.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高階特性系列,感謝閱