在上一篇博文发了一天左右的时间, 就收到了博客园许多读者的评论和推荐, 非常感谢, 我也会及时回复读者的评论. 之后我也将继续撰写博文, 梳理相关. 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 的同时, 也是在锻炼你的基本功是否扎实, 你是否对这门语言精通, 是否有各种简化代码的应用.
来源: https://www.cnblogs.com/billming/p/emit-study-class.html