C#的System.Reflection.Emit教程 - 显著改善反射性能
第一部分:Emit介绍
Emit 是开发者在掌握反射的使用后,进阶需要的知识,它能显著的改善因反射带来的性能影响
。
Emit 是一种动态生成IL代码
的技术,通过使用 System.Reflection.Emit
命名空间中的类,可以在运行时创建和修改程序集、类型和方法。
Emit 技术通常用于解决需要在运行时动态生成代码的特定场景,例如在ORM(对象关系映射)框架中动态创建实体类,或者在AOP(面向切面编程)中动态创建代理类。
使用 Emit 技术的步骤通常包括以下几个步骤:
1、创建一个动态程序集(AssemblyBuilder)。
2、在程序集中创建一个动态模块(ModuleBuilder)。
3、在模块中创建一个动态类型(TypeBuilder)。
4、在类型中创建动态方法(MethodBuilder)。
5、使用ILGenerator编写实际的IL代码,包括加载、存储和计算等操作。
6、完成IL代码的生成后,使用CreateType方法将动态类型创建为实际的类型。
7、最后,通过反射或创建委托或其他方式,可以在运行时调用动态生成的方法。
更通常情况下,使用创建动态方法更常见,即从第4步开始,使用 DynamicMethod 直接创建动态方法。
第二部分:构建动态程序集(AssemblyBuilder)
程序集
是.NET中的基本部署单位,它包含了可执行代码、资源、元数据等信息,是.NET应用程序的基本组成单元之一。
静态程序集
(即程序集持久化)不同,动态程序集
是在运行时生成的,使得我们可以根据需要动态地构建和加载程序集。
在C#中,操作程序集的核心类是:AssemblyBuilder
。
在这过程,我们可以使用 AssemblyName
来定义程序集的名称和版本等信息。
AssemblyName assemblyName = new AssemblyName("MyDllName") { Version = new Version("1.0.0.0") };
.NET Core 代码:通过 AssemblyBuilder.DefineDynamicAssembly
来创建动态程序集对象:
AssemblyBuilder ab = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
AssemblyBuilderAccess
在 .Net Core 系列中的属性:
AssemblyBuilderAccess.Run
表示程序集可被执行,但不能被保存。AssemblyBuilderAccess.Collect
表示程序集可以被卸载并且内存会被回收
可以看到,目前版本是不支持持久化功能。
第三部分:构建模块(Module)
模块
是动态程序集中的基本单位,它类似于一个独立的代码单元,可以包含类型
、方法
、字段
等成员。在动态程序集中,模块扮演着组织代码和实现代码复用的关键角色。一个程序集可以包含一个或多个模块,这种模块化的设计有助于提高代码的可维护性和可扩展性。
通过 AssemblyBuilder
的 DefineDynamicModule
方法,即可获得 ModuleBuilder
(后续章节会通过它,来添加类型、方法、字段等成员到模块中,从而构建出所需的模块结构)。
AssemblyBuilder ab = ......
ModuleBuilder mb = ab.DefineDynamicModule("一个名称");
第四部分:构建类型(Type)
在动态生成代码的过程中,构建类型(Type
)是至关重要的一步。通过使用 Emit
中的 TypeBuilder
,我们可以定义和创建各种类型,包括类、结构体和接口。
// 定义类型
TypeBuilder tb = mb.DefineType("MyClass", TypeAttributes.Public);
// 定义接口
TypeBuilder tb = mb.DefineType("MyNameSpace.MyInterface", TypeAttributes.Public | TypeAttributes.Abstract | TypeAttributes.Interface);
// 定义结构体
TypeBuilder tb = mb.DefineType("MyNameSpace.MyStruct", TypeAttributes.SequentialLayout | TypeAttributes.Public | TypeAttributes.Sealed, typeof(ValueType));
// 定义枚举
EnumBuilder eb = mb.DefineEnum("MyNameSpace.MyEnum", TypeAttributes.Public, typeof(int));
// 定义抽象类
TypeBuilder tb = mb.DefineType("MyNameSpace.MyClassBase", TypeAttributes.Public | TypeAttributes.Abstract | TypeAttributes.Class);
tb.CreateType();
// 定义字段
// FieldAttributes.Public| FieldAttributes.Static| FieldAttributes.InitOnly
tb.DefineField("ID", typeof(int), FieldAttributes.Public);
tb.DefineField("Name", typeof(string), FieldAttributes.Public);
// 定义属性 "Name",类型为 int
PropertyBuilder propertyBuilder = tb.DefineProperty("Name", PropertyAttributes.None, typeof(int), null);
// 定义属性的 getter 方法
MethodBuilder getterMethodBuilder = tb.DefineMethod("get_Name", MethodAttributes.Public | MethodAttributes.Abstract | MethodAttributes.Virtual, typeof(int), Type.EmptyTypes);
propertyBuilder.SetGetMethod(getterMethodBuilder);
// 定义属性的 setter 方法
MethodBuilder setterMethodBuilder = tb.DefineMethod("set_Name", MethodAttributes.Public | MethodAttributes.Abstract | MethodAttributes.Virtual, null, new Type[] { typeof(int) });
propertyBuilder.SetSetMethod(setterMethodBuilder);
//定义方法 GetMyName
MethodBuilder getMyName = tb.DefineMethod("GetMyName", MethodAttributes.Public | MethodAttributes.Abstract | MethodAttributes.Virtual, typeof(string), new Type[] { typeof(int) });
第五部分:动态生成方法(MethodBuilder 与 DynamicMethod)
当我们涉及到在运行时生成和定义方法时,便需要使用到C#中的两个关键类之一:MethodBuilder
或 DynamicMethod
。
MethodBuilder
如果你需要构建整个类型
(包括字段、属性、方法等),那么按第一部分讲的:1-7 依次创建下来, 在第4步就可以使用其创建方法。
特点:
- 绑定到类型:
MethodBuilder
创建的方法是属于某个类型的一部分,因此只能通过该类型的实例或静态引用来调用。 - 方法签名:需要指定方法名称、参数类型和返回类型。
DynamicMethod
DynamicMethod
则更加灵活。它允许在运行时生成和执行方法,而无需创建动态程序集或动态类型来容纳该方法。
优点:
- 不绑定到类型:
DynamicMethod
不属于特定的类型,因此可以在任何上下文中调用。 - 性能比
MethodBuilder
更高 - 可以直接调用
dynamicMethod.CreateDelegate()
调用委托
两者的区别:
DynamicMethod:
DynamicMethod
类允许在运行时生成和执行方法,而无需创建动态程序集或动态类型来容纳该方法。- 由即时(JIT)编译器生成的可执行代码在
DynamicMethod
对象被回收时也会被回收。 - 动态方法是生成和执行少量代码的最有效方式。
- 如果需要动态创建一个或多个方法,应使用
DynamicMethod
。
MethodBuilder:
MethodBuilder
用于在动态程序集中创建方法。- 如果要创建整个类型(包括字段、属性、方法等),则需要先创建一个动态程序集(
AssemblyBuilder
),然后在其中创建一个模块(ModuleBuilder
),最后再创建一个或多个类型(TypeBuilder
)。 - 若要在这些类型中创建方法,可以使用
MethodBuilder
。
总结:
- 如果是生成动态程序集,包括创建动态类,那么使用
MethodBuilder
。 - 如果只是定义动态方法供调用,使用
DynamicMethod
,因为它不用定义整个程序集,直接起手就是方法。 - 使用委托调用方法:
MethodBuilder
和DynamicMethod
都支持,但DynamicMethod
直接提供CreateDelegate
方法,方便起手调用。
第五部分:动态生成IL代码(ILGenerator)
通过ILGenerator,开发人员可以在运行时创建和修改方法体内的IL指令,实现动态方法的生成和优化。
在ILGenerator方法中有两种方法
- 指令方法:il.Emit(OpCodes.Ret), 用于执行IL指令。
- 辅助方法:il.EmitWriteLine("hello world!")等, 是基于IL指令封装的一些常用方法,方便调用,不过辅助方法目前很少。
第六部分:IL 指令(难点与核心)
工具: 在线运行程序,且可以查看生成的IL代码 https://sharplab.io/
在.NET平台上,IL(Intermediate Language)是一种中间语言。它是由高级语言(如C#、VB.NET等)编译而成的一种低级语言表示形式。IL 代码被保存在 .net 程序集中,并由公共语言运行时(CLR)执行。
C#--编译-->IL--CLR-->机器码
通过学习和理解IL代码,可以更深入地了解.NET程序的内部工作原理,为程序的优化和调试提供帮助。
IL 指令的结构和格式
官方文档中查看IL指令: https://learn.microsoft.com/zh-cn/dotnet/api/system.reflection.emit.opcodes?view=net-8.0
IL 指令由操作码(OpCode)和操作数(Operand)两部分组成。
- 操作码:表示要执行的操作,如加载数据、执行算术运算、跳转等。
- 操作数:提供给操作码的参数,用于指定要操作的数据或执行的具体行为。
IL指令通常以字节形式存储在 .net 程序集中,CLR在执行时会逐条解释和执行这些指令。
A、 加载和存储指令示例
- ldarg.0:将第一个参数加载到栈顶。
- ldloc.1:将第二个本地变量加载到栈顶。
- stloc.2:将栈顶的值存储到第三个本地变量中。
B、 算术和逻辑指令示例
- add:将栈顶两个值相加。
- mul:将栈顶两个值相乘。
- and:对栈顶两个值执行按位与操作。
C、 控制流指令示例
- br label:无条件跳转到标记为label的位置。
- beq label:如果相等则跳转到标记为label的位置。
- call method:调用指定的方法。
D、 对象模型指令示例
- newobj:创建一个新对象实例。
- ldfld:将对象字段的值加载到栈顶。
- callvirt method:调用虚方法。
E、 方法调用指令示例
- call method:调用静态方法。
- callvirt method:调用实例方法或虚方法。
- ret:从当前方法返回。
第七部分:实战项目
/// <summary>
/// 设置字段的值, 委托方法
/// </summary>
/// <typeparam name="TObj"></typeparam>
/// <typeparam name="TField"></typeparam>
/// <param name="fieldinfo"></param>
/// <returns></returns>
private static Action<TObj, TValue> CreateSetFieldDelegate<TObj, TValue>(FieldInfo fieldinfo)
{
// 创建动态方法
// 动态方法的名称 SetField
// 一个 Type 对象,用于指定动态方法的返回类型 typeof(void)
// 指定动态方法的参数类型的 Type 对象数组 [typeof(TObj), typeof(TValue)]
// 一个 Module,表示动态方法将与之逻辑关联的模块 typeof(Program).Module
var method = new DynamicMethod("SetField", typeof(void), [typeof(TObj), typeof(TValue)], typeof(Program).Module);
// 返回一个 IL 生成器,该生成器可用于构造动态方法的主体。
ILGenerator il = method.GetILGenerator();
// il.Emit() 将指令放到实时 (JIT) 编译器的 Microsoft 中间语言 (MSIL) 流上。
il.Emit(OpCodes.Ldarg_0); // 将第一个参数加载到栈顶。
il.Emit(OpCodes.Ldarg_1); // 将第二个本地变量加载到栈顶。
// Stfld指令,需要先接收一个对象(指针)Ldarg_0,在接收一个值Ldarg_1,再将Ldarg_0的fieldinfo设置成新的值Ldarg_1
il.Emit(OpCodes.Stfld, fieldinfo);
il.Emit(OpCodes.Ret); // 从当前方法返回,并将返回值(如果存在)从被调用方的计算堆栈推送到调用方的计算堆栈上。
// 调用CreateDelegate()创建动态方法的委托
// 该委托方法需要传入两个参数 TObj + TValue
return (Action<TObj, TValue>)method.CreateDelegate(typeof(Action<TObj, TValue>));
}
// 使用
public MyClass { int i = 10; }
FieldInfo _field = typeiof(MyClass).getField("i");
Action<MyClass, int> actions = CreateSetFieldDelegate<MyClass, int>(_field); // 生成委托方法,实际actions方法指向的是SetField方法
MyClass _cls = new MyClass();
actions(_cls, 100); // 调用委托方法 _cls.i = 100
第八部分:性能优化与注意事项
扩展阅读
最后更新于 2024-04-11 00:47:12 并被添加「」标签,已有 1905 位童鞋阅读过。
本站使用「署名 4.0 国际」创作共享协议,可自由转载、引用,但需署名作者且注明文章出处
此处评论已关闭