前面,本系列一共寫了 九 篇關於反射和特性相關的文章,講解了如何從程序集中經過反射將信息解析出來,以及實例化類型。api
前面的九篇文章中,重點在於讀數據,使用已經構建好的數據結構(元數據等),接下來,咱們將學習 .NET Core 中,關於動態構建代碼的知識。數據結構
其中表達式樹已經在另外一個系列寫了,因此本系列主要是講述 反射,Emit ,AOP 等內容。框架
若是如今總結一下,反射,與哪些數據結構相關?ide
咱們能夠從 AttributeTargets 枚舉中窺見:函數
public enum AttributeTargets { All=16383, Assembly=1, Module=2, Class=4, Struct=8, Enum=16, Constructor=32, Method=64, Property=128, Field=256, Event=512, Interface=1024, Parameter=2048, Delegate=4096, ReturnValue=8192 }
分別是程序集、模塊、類、結構體、枚舉、構造函數、方法、屬性、字段、事件、接口、參數、委託、返回值。工具
以往的文章中,已經對這些進行了很詳細的講解,咱們能夠中反射中得到各類各樣的信息。固然,咱們也能夠經過動態代碼,生成以上數據結構。學習
動態代碼的其中一種方式是表達式樹,咱們還可使用 Emit 技術、Roslyn 技術來編寫;相關的框架有 Natasha、CS-Script 等。ui
首先咱們引入一個命名空間:加密
using System.Reflection.Emit;
Emit 命名空間中裏面有不少用於構建動態代碼的類型,例如 AssemblyBuilder
,這個類型用於構建程序集。類推,構建其它數據結構例如方法屬性,則有 MethodBuilder
、PropertyBuilder
。spa
AssemblyBuilder 類型定義並表示動態程序集,它是一個密封類,其定義以下:
public sealed class AssemblyBuilder : Assembly
AssemblyBuilderAccess 定義動態程序集的訪問模式,在 .NET Core 中,只有兩個枚舉:
枚舉 | 值 | 說明 |
---|---|---|
Run | 1 | 能夠執行但沒法保存該動態程序集。 |
RunAndCollect | 9 | 當動態程序集再也不可供訪問時,將自動卸載該程序集,並回收其內存。 |
.NET Framework 中,有 RunAndSave 、Save 等枚舉,可用於保存構建的程序集,可是在 .NET Core 中,是沒有這些枚舉的,也就是說,Emit 構建的程序集只能在內存中,是沒法保存成 .dll 文件的。
另外,程序集的構建方式(API)也作了變動,若是你百度看到文章 AppDomain.CurrentDomain.DefineDynamicAssembly
,那麼你能夠關閉建立了,說明裏面的不少代碼根本沒法在 .NET Core 下跑。
好了,再也不贅述,咱們來看看建立一個程序集的代碼:
AssemblyName assemblyName = new AssemblyName("MyTest"); AssemblyBuilder assBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
構建程序集,分爲兩部分:
一個完整的程序集,有不少信息的,版本、做者、構建時間、Token 等,這些可使用
AssemblyName 來設置。
通常一個程序集須要包含如下內容:
你能夠參考如下示例:
AssemblyName assemblyName = new AssemblyName("MyTest"); assemblyName.Name = "MyTest"; // 構造函數中已經設置,此處能夠忽略 // Version 表示程序集、操做系統或公共語言運行時的版本號. // 構造函數比較多,能夠選用 主版本號、次版本號、內部版本號和修訂號 // 請參考 https://docs.microsoft.com/zh-cn/dotnet/api/system.version?view=netcore-3.1 assemblyName.Version = new Version("1.0.0"); assemblyName.CultureName = CultureInfo.CurrentCulture.Name; // = "zh-CN" assemblyName.SetPublicKeyToken(new Guid().ToByteArray());
最終程序集的 AssemblyName 顯示名稱是如下格式的字符串:
Name <,Culture = CultureInfo> <,Version = Major.Minor.Build.Revision> <, StrongName> <,PublicKeyToken> '\0'
例如:
ExampleAssembly, Version=1.0.0.0, Culture=en, PublicKeyToken=a5d015c7d5a0b012
另外,建立程序集構建器使用 AssemblyBuilder.DefineDynamicAssembly()
而不是 new AssemblyBuilder()
。
程序集和模塊之間的區別能夠參考
https://stackoverflow.com/questions/9271805/net-module-vs-assembly
https://stackoverflow.com/questions/645728/what-is-a-module-in-net
模塊是程序集內代碼的邏輯集合,每一個模塊可使用不一樣的語言編寫,大多數狀況下,一個程序集包含一個模塊。程序集包括了代碼、版本信息、元數據等。
MSDN指出:「模塊是沒有 Assembly 清單的 Microsoft 中間語言(MSIL)文件。」。
這些就再也不扯淡了。
建立完程序集後,咱們繼續來建立模塊。
AssemblyName assemblyName = new AssemblyName("MyTest"); AssemblyBuilder assBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run); ModuleBuilder moduleBuilder = assBuilder.DefineDynamicModule("MyTest"); // ⬅
目前步驟:
Assembly -> Module -> Type 或 Enum
ModuleBuilder 中有個 DefineType
方法用於建立 class
和 struct
;DefineEnum
方法用於建立 enum
。
這裏咱們分別說明。
建立類或結構體:
TypeBuilder typeBuilder = moduleBuilder.DefineType("MyTest.MyClass",TypeAttributes.Public);
定義的時候,注意名稱是完整的路徑名稱,即命名空間+類型名稱。
咱們能夠先經過反射,獲取已經構建的代碼信息:
Console.WriteLine($"程序集信息:{type.Assembly.FullName}"); Console.WriteLine($"命名空間:{type.Namespace} , 類型:{type.Name}");
結果:
程序集信息:MyTest, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null 命名空間:MyTest , 類型:MyClass
接下來將建立一個枚舉類型,而且生成枚舉。
咱們要建立一個這樣的枚舉:
namespace MyTest { public enum MyEnum { Top = 1, Bottom = 2, Left = 4, Right = 8, All = 16 } }
使用 Emit 的建立過程以下:
EnumBuilder enumBuilder = moduleBuilder.DefineEnum("MyTest.MyEnum", TypeAttributes.Public, typeof(int));
TypeAttributes 有不少枚舉,這裏只須要知道聲明這個枚舉類型爲 公開的(Public);typeof(int)
是設置枚舉數值基礎類型。
而後 EnumBuilder 使用 DefineLiteral
方法來建立枚舉。
方法 | 說明 |
---|---|
DefineLiteral(String, Object) | 在枚舉類型中使用指定的常量值定義命名的靜態字段。 |
代碼以下:
enumBuilder.DefineLiteral("Top", 0); enumBuilder.DefineLiteral("Bottom", 1); enumBuilder.DefineLiteral("Left", 2); enumBuilder.DefineLiteral("Right", 4); enumBuilder.DefineLiteral("All", 8);
咱們可使用反射將建立的枚舉打印出來:
public static void WriteEnum(TypeInfo info) { var myEnum = Activator.CreateInstance(info); Console.WriteLine($"{(info.IsPublic ? "public" : "private")} {(info.IsEnum ? "enum" : "class")} {info.Name}"); Console.WriteLine("{"); var names = Enum.GetNames(info); int[] values = (int[])Enum.GetValues(info); int i = 0; foreach (var item in names) { Console.WriteLine($" {item} = {values[i]}"); i++; } Console.WriteLine("}"); }
Main 方法中調用:
WriteEnum(enumBuilder.CreateTypeInfo());
接下來,類型建立成員,就複雜得多了。
下面咱們來爲 類型建立一個方法,並經過 Emit 向程序集中動態添加 IL。這裏並非使用 MethodBuider,而是使用 DynamicMethod。
在開始以前,請自行安裝反編譯工具 dnSpy 或者其它工具,由於這裏涉及到 IL 代碼。
這裏咱們先忽略前面編寫的代碼,清空 Main 方法。
咱們建立一個類型:
public class MyClass{}
這個類型什麼都沒有。
而後使用 Emit 動態建立一個 方法,而且附加到 MyClass 類型中:
// 動態建立一個方法而且附加到 MyClass 類型中 DynamicMethod dyn = new DynamicMethod("Foo",null,null,typeof(MyClass)); ILGenerator iLGenerator = dyn.GetILGenerator(); iLGenerator.EmitWriteLine("HelloWorld"); iLGenerator.Emit(OpCodes.Ret); dyn.Invoke(null,null);
運行後會打印字符串。
DynamicMethod 類型用於構建方法,定義並表示能夠編譯、執行和丟棄的一種動態方法。 丟棄的方法可用於垃圾回收。。
ILGenerator 是 IL 代碼生成器。
EmitWriteLine 做用是打印字符串,
OpCodes.Ret 標記 結束方法的執行,
Invoke 將方法轉爲委託執行。
上面的示例比較簡單,請認真記一下。
下面,咱們要使用 Emit 生成一個這樣的方法:
public int Add(int a,int b) { return a + b; }
看起來很簡單的代碼,要用 IL 來寫,就變得複雜了。
ILGenerator 正是使用 C# 代碼的形式去寫 IL,可是全部過程都必須按照 IL 的步驟去寫。
其中最重要的,即是 OpCodes 枚舉了,OpCodes 有幾十個枚舉,表明了 IL 的全部操做功能。
請參考:https://docs.microsoft.com/zh-cn/dotnet/api/system.reflection.emit.opcodes?view=netcore-3.1
若是你點擊上面的連接查看 OpCodes 的枚舉,你能夠看到,不少 功能碼,這麼多功能碼是記不住的。咱們如今剛開始學習 Emit,這樣就會難上加難。
因此,咱們要先下載可以查看 IL 代碼的工具,方便咱們探索和調整寫法。
咱們看看此方法生成的 IL 代碼:
.method public hidebysig instance int32 Add( int32 a, int32 b ) cil managed { .maxstack 2 .locals init ( [0] int32 V_0 ) // [14 9 - 14 10] IL_0000: nop // [15 13 - 15 26] IL_0001: ldarg.1 // a IL_0002: ldarg.2 // b IL_0003: add IL_0004: stloc.0 // V_0 IL_0005: br.s IL_0007 // [16 9 - 16 10] IL_0007: ldloc.0 // V_0 IL_0008: ret } // end of method MyClass::Add
看不懂徹底不要緊,由於筆者也看不懂。
目前咱們已經得到了上面兩大部分的信息,接下來咱們使用 DynamicMethod
來動態編寫方法。
定義 Add 方法並獲取 IL 生成工具:
DynamicMethod dynamicMethod = new DynamicMethod("Add",typeof(int),new Type[] { typeof(int),typeof(int)}); ILGenerator ilCode = dynamicMethod.GetILGenerator();
DynamicMethod 用於定義一個方法;ILGenerator是 IL 生成器。固然也能夠將此方法附加到一個類型中,完整代碼示例以下:
// typeof(Program),表示將此動態編寫的方法附加到 MyClass 中 DynamicMethod dynamicMethod = new DynamicMethod("Add", typeof(int), new Type[] { typeof(int), typeof(int) },typeof(MyClass)); ILGenerator ilCode = dynamicMethod.GetILGenerator(); ilCode.Emit(OpCodes.Ldarg_0); // a,將索引爲 0 的自變量加載到計算堆棧上。 ilCode.Emit(OpCodes.Ldarg_1); // b,將索引爲 1 的自變量加載到計算堆棧上。 ilCode.Emit(OpCodes.Add); // 將兩個值相加並將結果推送到計算堆棧上。 // 下面指令不須要,默認就是彈出計算堆棧的結果 //ilCode.Emit(OpCodes.Stloc_0); // 將索引 0 處的局部變量加載到計算堆棧上。 //ilCode.Emit(OpCodes.Br_S); // 無條件地將控制轉移到目標指令(短格式)。 //ilCode.Emit(OpCodes.Ldloc_0); // 將索引 0 處的局部變量加載到計算堆棧上。 ilCode.Emit(OpCodes.Ret); // 即 return,從當前方法返回,並將返回值(若是存在)從被調用方的計算堆棧推送到調用方的計算堆棧上。 // 方法1 Func<int, int, int> test = (Func<int, int, int>)dynamicMethod.CreateDelegate(typeof(Func<int, int, int>)); Console.WriteLine(test(1, 2)); // 方法2 int sum = (int)dynamicMethod.Invoke(null, BindingFlags.Public, null, new object[] { 1, 2 }, CultureInfo.CurrentCulture); Console.WriteLine(sum);
實際以上代碼與咱們反編譯出來的 IL 編寫有所差別,具體俺也不知道爲啥,在羣裏問了調試了,註釋掉那麼幾行代碼,才經過的。