前面,本系列一共写了 九 篇关于反射和特性相关的文章,讲解了如何从程序集中经过反射将信息解析出来,以及实例化类型。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 编写有所差别,具体俺也不知道为啥,在群里问了调试了,注释掉那么几行代码,才经过的。