[料理佳餚] 在執行時期(Runtime)憑空捏造一個型別(Type)

先前的文章有提到過「在執行時期憑空捏造一個組件」這件事,我是用在自己打造的 Library 之中,而做這件事的最主要目的是在執行時期產生一個類別,用來產生使用端類別的替身,或是用來做一些特殊的識別,我們就來看一下,要在執行時期產生一個類別,需要做哪些事情?

首先我們要知道,屬性(Property)、欄位(Field)、方法(Method)是隸屬於類別(Type),會成為類別的成員(Member),而類別隸屬於模組(Module),模組則隸屬於組件(Assembly),因此我們必須先從 Assembly 開始一路建下去。

產生組件

var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName("MyAssembly"), AssemblyBuilderAccess.Run);

產生模組

var moduleBuilder = assemblyBuilder.DefineDynamicModule("MyAssembly");

產生類別

var typeBuilder = moduleBuilder.DefineType("MyType", TypeAttributes.Public);

產生屬性

var propertyBuilder = typeBuilder.DefineProperty("MyProperty", PropertyAttributes.HasDefault, typeof(string), null);

產生欄位

var fieldBuilder = typeBuilder.DefineField("myField", typeof(string), FieldAttributes.Private);

產生方法

var methodBuilder = typeBuilder.DefineMethod("MyMethod", MethodAttributes.Public, typeof(int), new[] { typeof(int), typeof(int) });

var ilGenerator = methodBuilder.GetILGenerator();

// 傳入 2 個 int 相加之後回傳
ilGenerator.Emit(OpCodes.Ldarg_1);
ilGenerator.Emit(OpCodes.Ldarg_2);
ilGenerator.Emit(OpCodes.Add);
ilGenerator.Emit(OpCodes.Ret);
為何載入參數的索引從 1 開始? 因為索引 0 的參數預設是 this。

最後,我們只要呼叫 TypeBuilder.CreateType() 方法就能建立類別,透過 Type.InvokeMember() 方法就可以簡單測試一下我們所建立的方法,執行結果正不正確?

不過,因為這些都是在執行時期才會產生的,所以我們沒辦法在編譯的時候就預先知道錯誤,再來就是我們沒有該類別實際上的程式碼,所以也不容易除錯,這些都是我們要使用這種方式建立類別所要考量的。

完整的程式碼

var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName("MyAssembly"), AssemblyBuilderAccess.Run);

var moduleBuilder = assemblyBuilder.DefineDynamicModule("MyAssembly");

var typeBuilder = moduleBuilder.DefineType("MyType", TypeAttributes.Public);

var propertyBuilder = typeBuilder.DefineProperty("MyProperty", PropertyAttributes.HasDefault, typeof(string), null);

var fieldBuilder = typeBuilder.DefineField("myField", typeof(string), FieldAttributes.Private);

var methodBuilder = typeBuilder.DefineMethod("MyMethod", MethodAttributes.Public, typeof(int), new[] { typeof(int), typeof(int) });

var ilGenerator = methodBuilder.GetILGenerator();

// 傳入 2 個 int 相加之後回傳
ilGenerator.Emit(OpCodes.Ldarg_1);
ilGenerator.Emit(OpCodes.Ldarg_2);
ilGenerator.Emit(OpCodes.Add);
ilGenerator.Emit(OpCodes.Ret);

var myType = typeBuilder.CreateType();

var myObj = Activator.CreateInstance(myType);

var result = myType.InvokeMember(
    "MyMethod",
    BindingFlags.InvokeMethod | BindingFlags.Instance | BindingFlags.Public,
    null,
    myObj,
    new object[] { 1, 2 });

參考資料

C# 指南ASP.NET 教學ASP.NET MVC 指引
Azure SQL Database 教學SQL Server 教學Xamarin.Forms 教學