(翻译)反射的第二部分:Emit
作者:Mike Snell,Lars Powers
翻译:today
在我们之前的文章,反射的第一部分:发现和执行里,我们已经介绍了System.Reflection命名空间及其包含的类,开发人员使用这些类可以查看程序集的元数据,并且可以在运行时查找和发现类型,甚至可以调用发现的代码。在这篇文章中,我们将探讨反射的高级功能:Emit,它具有在运行时动态的产生代码的功效。
回顾反射
首先,让我们快速的回顾一下,什么是反射以及反射可以被用来做什么。从第一部分内容中,你已经知道,反射是在运行时发现对象的相关信息,并且执行这些对象(创建对象实例,执行对象上的方法)。这个功能是由.NET的System.Reflection命名空间的类所提供的。这些被用于类型发现和动态调用的类包括:Assembly,Module,ConstructorInfo,MethodInfo以及其它。简单的说,它们不仅允许你浏览一个程序集暴露的类、方法、属性和字段,而且还允许你创建一个类型的实例以及执行这些类型上的方法(调用成员)。这些特性对于在运行时对象发现,已经很了不起了,但.NET的反射机制并没有到此结束。反射还允许你在运行时构建一个程序集,并且可以创建全新的类型。这就是反射发出(reflection emit)。
何谓反射发出(Reflection Emit)
System..Reflection.Emit命名空间嵌套在System.Reflection的下面,它是,允许你从零开始,动态的构建程序集和类型的所有框架类的根。在需要时动态的产生代码,类似这样的操作,虽然很少的开发人员会需要,但这对于.NET框架是一种凭据,证明有这样的工具可以解决有这样需求的业务问题。
注意:反射发出(reflection emit)并不能产生源代码。换句话说,你在这里的努力并不能创建VB.Net或者C#代码。相反,反射发出(reflection emit)类会创建MSIL op代码。
作为例子,使用反射发出(reflection emit)可能会是这样子的:
1. 创建一个新的程序集(程序集是动态的存在于内存中或把它们持久化到磁盘上)。
2. 在程序集内部,创建一个模块(module)。
3. 在模块内部,创建一个类型。
4. 给类型添加属性和方法。
5. 产生属性和方法内部的代码
确切得说,当你使用Reflection.Emit类产生代码时,以上描述的是你实际中要遵循的过程。
代码生成的过程
依照上面列出的步骤,让我们探讨一下构建一个程序集,必要的操作。为此,我们举个非常简单的例子。假设你想构建一个类MathOps,它有一个公共的方法(函数),这个方法接收两个Integer类型的参数,然后返回它们的相加后的值。
第一步:构建程序集
稍微扩充一下上面列出的步骤,在实际的操作中,第一步更像是如下所述:
a) 创建一个AssemblyName(用于唯一标识和命名程序集)。
b) 获取当前应用程序域的一个引用(使用应用程序域提供的方法,返回AssemblyBuilder对象)。
c) 通过调用AppDomain.DefineDynamicAssembly产生一个AssemblyBuilder对象实例。
为了开始程序集的构建过程,你首先需要创建一个AssemblyName实例,用于标识你的程序集。如下:



接下来,你需要System.AppDomain类的一个实例。你可以从当前运行的线程实例中获取。

这两个实例创建以后,你现在就可以定义一个AssemblyBuilder变量,然后使用之前创建的AssemblyName和AppDomain的实例把它实例化。AssemblyBuilder类是整个反射发出的工作支架。它给你,从零开始构造一个新的程序集提供了主要的机制。除此之外,你还需要指定一个AssemblyBuilderAccess枚举值,它将表明,你是想把程序集写入磁盘,保存到内存,还是两者都有。在这个例子里,你想把程序集保存在内存里。



第二步:定义一个模块(module)
在第二步里,你将使用ModuleBuilder类,在你之前创建的程序集(builder)里创建一个动态的模块。ModuleBuilder用于在一个程序集中创建一个模块。调用AssemblyBuilder对象上的DefineDynamicModule方法将会返回一个ModuleBuilder对象实例。跟程序集一样,你必须给这个模块命名(在这里,名字仅仅是个字符窜)。



第三步:创建一个类型
现在,你已经拥有了一个程序集,一个模块,你可以把你的类添加到这个程序集中。为了创建一个新的类型,你需要使用TypeBuilder类。你将会使用一个方法(DefineType)从“父对象”(指mb)得到一个TypeBuilder对象的实例。



注意,你已经使用TypeAttributes枚举指定了该类型的可见度为公共的。
第四步:添加一个方法
既然所需的类型已经创建好了,那么你就可以给它添加方法了。即将添加的方法命名为ReturnSum,可见度为公共的。
使用MethodBuilder类可以为你指定的类型定义方法。你可以在之前创建的类型对象上(theClass)调用DefineMethod获取一个MethodBuilder实例的引用。DefineMethod携带四个参数:方法的名称,方法可能的属性(如:public,private等等),方法的参数以及方法的返回值。在子程序里,参数和返回值可以是void值。对于这个例子里即将创建的方法,你同时需要指定参数和返回值的类型。
为了定义返回值的类型,创建一个包含返回类型值的类型对象(一个System.Int32类型的值)



使用类型值数组定义方法的参数,这两个参数也是Int32的类型值。





有了这些值,你现在就可以调用DefineMethod方法了。



第五步:产生代码
截止到现在,在第四步里,方法的框架已基本上搭建起来,你现在需要做的是添加方法的内部代码。这是使用反射发出(reflection emit)产生代码的过程中真正核心的部分。
有一点是需要注意的,反射发出(reflection emit)的类不能产生源代码。换句话说,这里的结果并不会产生Visual Basic.NET或者C#代码,而是产生MSIL op 代码。MSIL(微软中间语言)是一种接近于汇编程序的中间代码语言。当.NET JIT 编译器产生本地二进制代码的时候,就需要编译MSIL。Op代码是低级的,类似于汇编程序的操作指令。
考虑方法ReturnSum的如下实现:





如果你想“发出”这一段代码,你首先需要知道如何仅使用MSIL op代码编写这个方法。值得高兴的是,这里有一个快速,简单的办法可以做到。你可以简单的编译一下这段代码,然后使用.NET框架里的实用工具ildasm.exe查看程序集的结果。以下MSIL版本的代码是编译上面的方法产生的:















为了产生这段代码,你需要使用ILGenerator类。你可以调用MethodBuilder.GetILGenerator()方法获取对应方法上的ILGenerator类的一个实例。换句话说,如下的代码将会获取你的方法ReturnMethod上的一个ILGenerator实例。

使用gen对象,你可以把op指令注入到你的方法里。








到此,你已经创建了方法,类,模块和程序集。为了得到这个类的一个引用,你可以调用CreateType,类似于下面的代码:

命名空间和类
正如你知道的那样,Reflection.Emit命名空间包含一系列核心“构建”类,它们用于创建类型和与新类型相关的,如:各种特性,方法,字段,属性等等。Table 1描述了使用反射产生代码用到的主要的类。
Table 1 反射发出相关类的参考
Namespace.Class |
System.Reflection.Emit.AssemblyBuilder |
主要用途 |
定义动态的.NET程序集:一种自我描述的 .NET内建块.动态程序集是通过反射发出特意产生的. 该类继承于System.Reflection.Assembly. |
范例 |
Dim ab As AssemblyBuilderDim ad As AppDomainad = Thread.GetDomain()ab = ad.DefineDynamicAssembly(name, AssemblyBuilderAccess.Run) |
Namespace.Class |
System.Reflection.Emit.ConstructorBuilder |
主要用途 |
用于创建和声明一个动态类的构造器.它囊括了有关构造器的所有信息,包括:名称,方法签名和主体代码.仅仅在你需要创建一个带参数的构造器或者需要覆盖父类构造器的默认行为的时候. |
范例 |
Dim ourClass As TypeBuilder = [module].DefineType("ourClass", _TypeAttributes.Public)Dim ctorArgs As Type() = {GetType(String)}Dim ctor As ConstructorBuilder = _ourClass.DefineConstructor(MethodAttributes.Public, _CallingConventions.Standard, constructorArgs) |
Namespace.Class |
System.Reflection.Emit.CustomAttributeBuilder |
主要用途 |
用于创建动态类的自定义特性. |
Namespace.Class |
System.Reflection.Emit.EnumBuilder |
主要用途 |
定义和声明枚举. |
Namespace.Class |
System.Reflection.Emit.EventBuilder |
主要用途 |
为动态类创建事件. |
Namespace.Class |
System.Reflection.Emit.FieldBuilder |
主要用途 |
为动态类创建字段. |
Namespace.Class |
System.Reflection.Emit.ILGenerator |
主要用途 |
用于产生MSIL代码. |
范例 |
Dim gen As ILGenerator = someMethod.GetILGenerator()gen.Emit(OpCodes.Ldarg_0)gen.Emit(OpCodes.Ret) |
Namespace.Class |
System.Reflection.Emit.LocalBuilder |
主要用途 |
创建方法或构造器的局部变量. |
Namespace.Class |
System.Reflection.Emit.MethodBuilder |
主要用途 |
用于创建和声明动态类的方法. |
Namespace.Class |
System.Reflection.Emit.MethodRental |
主要用途 |
一个很实用的类,用于从别的类中交换一个方法到动态创建的类中。当你需要快速重建一个已经在其它地方存在的方法时,就显得非常有用。 |
Namespace.Class |
System.Reflection.Emit.ParameterBuilder |
主要用途 |
为方法的签名创建参数. |
Namespace.Class |
System.Reflection.Emit.PropertyBuilder |
主要用途 |
为动态的类型创建属性. |
把反射发出和动态调用结合起来
现在你已经知道,如何使用反射类“发出”一个动态的程序集,那么让我们把反射发出和动态调用的内容(在第一部分讲到的)结合起来。
举个例子,在运行时何时使用Reflection和Reflection.Emit胜过代码或脚本赋值呢?这是有可能的,例如,显示一个带有输入框的窗体,要求用户输入一个公式,然后在运行时通过编译后的代码,求这个公式的值。
另外一种使用Reflection.Emit的时候,是为了使性能达到最优化。针对某一个问题,编码的解决方案,有时候故意的趋向于通用的解决方案。从设计的角度出发,这通常都是一件好事情,因为这会使你的系统更具有灵活性。例如,如果你想计算一些数字的和,在你设计的时候不必关心有多少个数字需要求和,因此你需要调用一个循环来解决这样的问题。如果你重写ReturnSum方法,让它接收一个Integer型的数组,你就需要在这个数组的成员之间循环,把每一个加到计数器上,然后返回所有数字的求和值。这是一个非常好的,通用的解决方法,因为它不必关心包含在数组中的值。










另一方面,如果你硬编码数组的界限,那么你就可以通过编写一个长的数字操作语句来求和,这样的方式将会使代码达到更优化的状态。对于少量的值甚至几百个值而言,这两种编写方式带来的性能上的差距是可以忽略的。但是,如果你正在处理数千或者数百万的值,硬编码的方式将会非常非常的快。事实上,你可以把这个方法编写得更快,直接把数组中的值取出来相加,同时,把不影响结果的零值去掉。






当然,这里的问题是,你编写的代码是不通用的和没有灵活性的。
因此,如何能够同时得到这两者的优点呢?答案是:使用Reflection.Emit。
通过把Reflection.Emit的功能(接收数组的上限和数组的值,然后产生数字直接相加的代码)和Reflection的功能(定位,加载并运行发出的程序集)融合在一起,你将能够打造出优雅的,具有独创性的性能解决方案,从而很好的避免了脆弱的代码。在这个简单的例子里,你可以写一个循环语句,产生你需要的MSIL op代码。
考虑下面的控制台程序,它接收一个数组,并创建一个新的程序集,模块,类和ReturnSum方法,它(ReturnSum)将直接求和数组中的值,而不是使用循环。代码如下:

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

总结
Reflection和Reflection.Emit允许程序员在运行时动态的创建和执行代码。它们提供了一组非常专门的类,来处理一些独特的、专门的业务问题。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构