使用 Reflection.Emit 创建动态类型






4.93/5 (80投票s)
在本文中,我提供了示例代码,以确保您可以使用 Reflection.Emit 类轻松构建自己的动态类型。
目录
大家好,很久没给你们写文章了。虽然我一直在为你们写技术博客,但真的很想分享一个有助于数据库的教程。最终决定开始写一个。
在努力学习 Reflection
类之后,我认为我可以对代码生成做一些研究。我选择了 CodeDom
作为生成代码的最佳替代方案。然而,我很快发现 CodeDom
实际上允许你构建你的程序集,但它不允许你在运行时动态编译程序集的一部分,而是调用编译器来完成。因此,我认为除了 CodeDom 之外,一定还有其他东西可以满足我的需求。
接下来我找到了一个,使用 Expression Trees
。如果你已经关注我了,我想你知道,几天前我已经写过关于表达式树和 Lambda 分解的文章。所以现在不是回顾同一主题的好时机。后来,我研究了 MSIL,发现它值得学习。如果你想在 .NET 中成长,了解 MSIL 将是一个额外的优势。因此,我开始研究 MSIL
。最后,我发现了一些可以帮助你动态构建 Type
的类。让我与你分享所有这些。
引言
Reflection.Emit
类似于 CodeDom
,它允许你构建自定义程序集,并提供许多 Builder 类,这些类可以在运行时编译,从而调用 C# 的 DLR 功能。该库还公开了一个 ILGenerator
,稍后可以通过发出操作码来生成实际的 MSIL。因此,当你正确编写操作码后,就可以在运行时动态编译类型。在这篇文章中,我将使用 ILDASM
来查看我们自己定义的类的生成 IL,然后我将尝试动态构建相同的类。
什么是 Reflection?
如果你对 Reflection 感到困惑,那么你需要真正振作起来才能进一步。让我简要解释一下 Reflection。Reflection 实际上是一种读取应用程序引用或未引用的托管 dll 并调用其类型的技术。换句话说,它是一种在运行时发现类型并调用其属性的机制。例如,你有一个外部 dll,它写入日志信息并发送到服务器。在这种情况下,你有两种选择。
- 你直接引用程序集并调用其方法。
- 你使用
Reflection
加载程序集并使用接口调用它。
如果您想为您的应用程序构建一个真正解耦的架构,例如可以稍后插入应用程序的架构,那么始终最好选择第二种选项。让我更清楚地说明一下,假设您希望客户从您的服务器下载日志 dll 并在需要时将其插入应用程序。相信我,除了使用 Reflection 别无选择。Reflection
类允许您将外部程序集加载到应用程序并在运行时调用其类型。
要了解更多信息,请尝试反射概述。
什么是 Reflection.Emit?
作为 Reflection 的一部分,Reflection.Emit 命名空间列出了许多可用于构建类型的类。正如我之前告诉你的,`Reflection.Emit` 实际上为你提供了一些 Builder 类,如 `AssemblyBuilder`、`ModuleBuilder`、`ConstructorBuilder`、`MethodBuilder`、`EventBuilder`、`PropertyBuilder` 等,它们允许你在运行时动态构建 IL。`ILGenerator` 提供了生成 IL 并将其放置在方法中的能力。通常,开发人员很少需要这些能力来在运行时生成程序集,但很高兴发现框架中存在这些能力。
现在让我们看看构建程序集需要什么。
生成程序集的步骤
现在让我们回顾一下创建程序集的步骤
- 在应用程序域中创建程序集。
AssemblyBuilder
将帮助您完成此操作。 - 在
Assembly
内部创建模块 - 在模块内部创建多个类型
- 在类型内部添加属性、方法、事件等。
- 使用
ILGenerator
写入属性、方法等。
基本上,这些是创建自己的动态生成程序集的常见步骤。

从上图中,您可以清楚地看到 CLR 程序集的整个结构。AppDomain
是层次结构的根,它创建 Assembly,然后是 Module,然后是 Type。如果您更仔细地查看 IL,您会理解 Delegate 也是一个继承自 System.MultiCastDelegate
的类,而 Struct 派生自 System.ValueTypes。每个类型可能包含其成员,每个成员方法或属性都可以有其 OPCodes、Locals 和 Parameters。Locals 定义您在方法体内部定义的局部变量,而 OpCodes 是 IL 的指令代码。
创建动态程序集的步骤
现在让我们一步一步地使用 IL 自己构建一个动态程序集。
步骤 1. 动态创建程序集
public AssemblyBuilder GetAssemblyBuilder(string assemblyName) { AssemblyName aname = new AssemblyName(assemblyName); AppDomain currentDomain = AppDomain.CurrentDomain; // Thread.GetDomain(); AssemblyBuilder builder = currentDomain.DefineDynamicAssembly(aname, AssemblyBuilderAccess.Run); return builder; }
要创建程序集,您需要程序集的名称才能唯一标识程序集。我使用了 AssemblyName
类来命名程序集。AppDomain
是程序集将被创建的地方。这非常重要,因为应用程序在调用跨域对象时可能会出现问题。为了使其更简单,我没有创建新的 AppDomain,而是使用程序正在运行的 CurrentDomain
。最后,我创建了一个 AssemblyBuilder
对象,它最终会使用唯一的名称 aname 构建程序集。AssemblyBuilderAccess
指定了程序集对我们的可访问性。正如我所做的那样,如果您使用 Run,这意味着程序集只能使用 Reflection 动态运行,而不能保存以备将来使用。浏览每个值以查看输出。
注意:如果您为程序集定义了自定义属性,您可以轻松使用 SetCustomAttriburte 将您的自定义属性添加到程序集。
AssemblyBuilder 的一些功能(供进一步阅读)
AssemblyBuilder
允许您定义许多功能,例如
AddResourceFile
:允许您指定要作为资源添加到程序集的文件。DefineUnmanagedResource / DefineResource
:为程序集添加一个非托管资源EntryPoint/SetEntryPoint
:要定义的一个特殊子例程/方法,当程序集被调用时将自动调用。SetCustomAttribute
:允许您为程序集指定属性。DefineDynamicModule
:为程序集定义模块,实际代码将包含在此模块中。
在构建程序集时有更多的灵活性。.NET 提供了我们通过库可以获得的所有功能,并公开了方法以确保我们可以通过 Reflection.Emit 来实现。您可以尝试 MSDN 阅读更多关于它公开的方法。
步骤 2:创建模块
模块是对象的一部分,实际的类将保留在那里。模块是我们放置所有类的容器。让我们为自己创建一个模块。
public ModuleBuilder GetModule(AssemblyBuilder asmBuilder) { ModuleBuilder builder = asmBuilder.DefineDynamicModule("EmitMethods", "EmitMethods.dll"); return builder; }
因此,该方法实际上需要一个 ModuleName
、一个唯一的模块名称以及程序集将导出到的文件名。
一些有用的方法
模块公开了一些方法,例如
DefineEnum
:允许您定义一个枚举,它返回一个 EnumBuilder。DefineType
:允许您定义一个类型/类。DefineManifestResource
:一个 dll 包含一个二进制清单。此方法允许您定义清单。DefinePInvokeMethod
:允许您为程序集定义一个 PInvoke 方法 (COM)。
步骤 3:创建类型
这才是关键。要创建一个类、结构、委托等,你需要定义一个 TypeBuilder。从现在开始,我将查看使用 ILDASM 生成的实际 IL,然后为你生成相同的输出。
public TypeBuilder GetType(ModuleBuilder modBuilder, string className) { TypeBuilder builder = modBuilder.DefineType(className, TypeAttributes.Public); return builder; } public TypeBuilder GetType(ModuleBuilder modBuilder, string className, params string[] genericparameters) { TypeBuilder builder = modBuilder.DefineType(className, TypeAttributes.Public); GenericTypeParameterBuilder[] genBuilders = builder.DefineGenericParameters( genericparameters); foreach (GenericTypeParameterBuilder genBuilder in genBuilders) // We take each generic type T : class, new() { genBuilder.SetGenericParameterAttributes( GenericParameterAttributes.ReferenceTypeConstraint | GenericParameterAttributes.DefaultConstructorConstraint); //genBuilder.SetInterfaceConstraints(interfaces); } return builder; }
上述 GetType
方法有两个重载。正如您所见,第一个很简单,我只指定了类的名称和 ModuleBuilder
,该方法返回 TypeBuilder
。
在第二个重载中,我放置了一个额外的字符串参数数组,它定义了类的每个泛型类型。GenericTypeParameterBuilder
允许您定义 GenericTypeParameter
。一旦您定义了 GenericTypeParameters
并设置了其约束属性,您就可以使用构建器了。
一些有用的方法
与类相比,TypeBuilder
允许您定义具有所有选项的完整结构。其中一些是
DefineField / DefineMethod / DefineProperties / DefineEvent
:您可以使用这些方法生成类成员。DefineMethodOverride
:允许您在类型继承自另一个基类型时覆盖现有方法DefineConstructor / DefineDefaultConstructor
:指定当前类型的构造函数。AddInterfaceImplementation
:允许您从另一个接口实现当前类型。
步骤 4:创建方法
方法是任何程序的构建块。我们将定义许多方法来阐明如何轻松地从 IL 构建方法。暂时,让我们使用 MethodBuilder
创建一个动态方法。
public MethodBuilder GetMethod(TypeBuilder typBuilder, string methodName) { MethodBuilder builder = typBuilder.DefineMethod(methodName, MethodAttributes.Public | MethodAttributes.HideBySig); return builder; } public MethodBuilder GetMethod(TypeBuilder typBuilder, string methodName, Type returnType, params Type[] parameterTypes) { MethodBuilder builder = typBuilder.DefineMethod(methodName, MethodAttributes.Public | MethodAttributes.HideBySig, CallingConventions.HasThis, returnType, parameterTypes); return builder; } public MethodBuilder GetMethod(TypeBuilder typBuilder, string methodName, Type returnType, string[] genericParameters, params Type[] parameterTypes) { MethodBuilder builder = typBuilder.DefineMethod(methodName, MethodAttributes.Public | MethodAttributes.HideBySig, CallingConventions.HasThis, returnType, parameterTypes); GenericTypeParameterBuilder[] genBuilders = builder.DefineGenericParameters(genericParameters); foreach (GenericTypeParameterBuilder genBuilder in genBuilders) // We take each generic type T : class, new() { genBuilder.SetGenericParameterAttributes( GenericParameterAttributes.ReferenceTypeConstraint | GenericParameterAttributes.DefaultConstructorConstraint); //genBuilder.SetInterfaceConstraints(interfaces); } return builder; }
因此,上述方法将返回 MethodBuilder
,它允许您定义 IL 代码。您可以看到我为此指定了 3 个重载。这些重载允许您为方法放置参数和泛型类型参数。
现在,构建类型后,您需要创建 Locals(局部变量)并使用 OpCode
指令。
使用 ILGenerator 发出操作码
为了定义您的 OpCodes
,您将需要 ILGenerator
。ILGenerator 允许您为方法体发出 IL,从而允许您为您即将构建的方法创建指令集。让我首先介绍一些指令集,它们可以帮助您将传入方法的两个整数变量相加,并返回一个浮点值作为结果。
public void CreateMethod() { AppDomain currentDomain = AppDomain.CurrentDomain; AssemblyBuilder asmbuilder = this.GetAssemblyBuilder("MyAssembly"); ModuleBuilder mbuilder = this.GetModule(asmbuilder); TypeBuilder tbuilder = this.GetTypeBuilder(mbuilder, "MyClass"); Type[] tparams = { typeof(System.Int32), typeof(System.Int32) }; MethodBuilder methodSum = this.GetMethod(tbuilder, "Sum", typeof(System.Single), tparams); ILGenerator generator = methodSum.GetILGenerator(); generator.DeclareLocal(typeof(System.Single)); generator.Emit(OpCodes.Ldarg_1); generator.Emit(OpCodes.Ldarg_2); generator.Emit(OpCodes.Add_Ovf); generator.Emit(OpCodes.Conv_R4); generator.Emit(OpCodes.Stloc_0); generator.Emit(OpCodes.Ldloc_0); generator.Emit(OpCodes.Ret); }
如果你仔细看上面的代码,代码实际上在 CurrentDomain
中创建了一个程序集,并在其中包含一个动态创建的类型 MyClass。因此,创建的类将包含 Sum 方法。
带有 ILGenerator
.Emit
的代码行实际上将 IL 发送到 Sum 方法体。每个方法都必须声明局部堆栈元素来运行其数据。在 IL 中,我们在调用任何指令代码之前声明局部变量。就像这样,我使用了 DeclareLocal
在方法中声明了一个 float32 局部变量。DeclareLocal
方法实际上返回一个 LocalBuilder
,您也可以使用它来操作此变量的索引。在声明所有局部变量之后,我们首先加载参数列表 Ldarg_1
和 Ldarg_2
(因为第一个参数是隐式对象 this
)。Add_Ovf
实际上将两个加载的参数相加并将其传递给局部变量 Stloc_0
(它表示堆栈的顶部元素或我们在索引 0 处创建的局部变量)。接下来,Ldloc_0
弹出值并将其返回给外部世界。
现在,这是生成您自己的类型的最简单的示例。现在让我进一步构建一个更具体的类型
一个更具体的示例
既然我们已经构建了自己的类型,是时候为您提供一个构建更具体类型的示例了。在我们演示之前,让我向您展示我们将如何在运行时动态创建代码,并调用其方法以获取输出。请注意,我已尝试在一定程度上简化代码,以便帮助您更好地理解代码。
public interface IBuilder { float Sum(int firstnum, int secondnum); float Substract(int firstnum, int secondnum); float Multiply(int firstnum, int secondnum); float Divide(int firstnum, int secondnum); } public class Builder : IBuilder { # region Event public delegate void BuilderDelegate(string message); public event BuilderDelegate InvokeMessage; public virtual void OnInvokeMessage(string message) { if (this.InvokeMessage != null) this.InvokeMessage(message); } # endregion # region Fields private int firstNum, secondNum; public int FirstNum { [DebuggerStepThrough()] get { return this.firstNum; } set { this.firstNum = value; } } [DebuggerBrowsable(DebuggerBrowsableState.Never)] public int SecondNum { get { return this.secondNum; } set { this.secondNum = value; } } # endregion # region Constructors public Builder(int firstnum, int secondnum) { this.FirstNum = firstnum; this.SecondNum = secondnum; } # endregion #region IBuilder Members public float Sum(int firstnum, int secondnum) { return firstnum + secondnum; } public float Substract(int firstnum, int secondnum) { return firstnum - secondnum; } public float Multiply(int firstnum, int secondnum) { return firstnum * secondnum; } public float Divide(int firstnum, int secondnum) { try { return firstnum / secondnum; } catch (DivideByZeroException ex) { Console.WriteLine("ZeroDivide exception : {0}", ex.Message); return 0; } } #endregion # region Methods public float GetProduct() { return this.Multiply(this.FirstNum, this.secondNum); } public override string ToString() { return string.Format("FirstNum : {0}, SecondNum : {1}", this.FirstNum, this.SecondNum); } # endregion }
在上面的类中,我实际上声明了一个接口 IBuilder
,后来我实现了它来生成一个类 Builder。该类包含一些方法、事件、属性等,以便您了解它所具有的每个灵活性。

看完代码后,我们打开 ILDASM,看看 IL 看起来像上面的图片一样。它基本上包含两种类型 .class
IBuilder
的.class
接口Builder
实现IBuilder
的.class
。
除此之外,您还会看到控制台应用程序 Main 方法的另一种类型,您可以暂时忽略它,因为我们将专注于 Type。
要构建动态类型,我们已经讨论过的最重要的事情是 Builder 类。BCL 公开了一些 Builder 类,使我们能够在运行时动态生成 MSIL 代码,因此您可以编译相同的代码以生成输出。
从上图中,我用红色标记了一些最重要的 Builder 类。但最终,您的应用程序需要运行指令。为了编写 IL 表达式,Reflection.Emit 提供了一个名为 ILGenerator
的类。ILGenerator(用蓝色标记)使您能够为方法或属性编写 IL。OpCodes
是确定计算机指令的操作码。因此,在编写指令时,您需要传递操作码并为方法生成指令集。
现在,继续我们的示例,让我逐一演示代码,以便您轻松构建自己的代码生成器。
为您的程序集实现 IBuilder
让我们转到 IBuilder
接口的实际实现。根据我们的讨论,IBuilder
包含 4 个成员:Sum、Divide、Multiply & Subtract
。
public interface IBuilder { float Sum(int firstnum, int secondnum); float Substract(int firstnum, int secondnum); float Multiply(int firstnum, int secondnum); float Divide(int firstnum, int secondnum); }
所以,因为我对 IL 还不熟悉,让我们用 ILDASM 看看 IL 到底长什么样,然后我们尝试为我们的程序集实现它。
嗯...在我构建并打开 ILDASM 反汇编我的程序集后,它生成的 IL 看起来像
.class interface public abstract auto ansi EmitMethodSample.IBuilder { .method public hidebysig newslot abstract virtual instance float32 Divide(int32 firstnum, int32 secondnum) cil managed { } .method public hidebysig newslot abstract virtual instance float32 Sum(int32 firstnum, int32 secondnum) cil managed { } .method public hidebysig newslot abstract virtual instance float32 Multiply(int32 firstnum, int32 secondnum) cil managed { } .method public hidebysig newslot abstract virtual instance float32 Substract(int32 firstnum, int32 secondnum) cil managed { } }
让我稍微解释一下 IL。
- 在 MSIL 中,任何类型都用 .class 定义,因此我们的 Type IBuilder 会得到一个 .class。
- interface, abstract 关键字将类型标识为抽象的,因此您不能创建该类型的对象。
- Auto 指定
LPSTR
(字符串长指针)自动解释。 - Ansi 指定
LPSTR
(字符串长指针)解释为 ANSI。 - IBuilder 中的方法使用 .method 关键字标识。
- 由于是接口成员,这些方法显示为抽象虚方法。
- instance 关键字指定对象为非静态
hidebysig
指定该方法可以通过名称和签名隐藏。您在 .NET 中定义的任何普通方法都具有这种灵活性。NewSlot
使成员在 vtable 中获得一个槽位。(vtable 是整个对象的内存区域。所以每当创建对象时,每个对象都会创建一个 vtable,并且在该对象内部创建的任何对象都会获得一个 vtable 条目。第一个成员作为隐藏指针可以用于查找 vtable 的成员)- cil managed 用于确定该方法在托管环境中实现。
现在您已经了解了接口生成的 IL,是时候开始创建类型 IBuilder
了。让我们为它编写代码
private Type CreateIBuilder(ModuleBuilder mbuilder) { TypeBuilder tbuilder = mbuilder.DefineType("IBuilder", TypeAttributes.Interface | TypeAttributes.Public | TypeAttributes.Abstract | TypeAttributes.AutoClass | TypeAttributes.AnsiClass); //Define Divide Type[] tparams = { typeof(System.Int32), typeof(System.Int32) }; MethodBuilder metDivide = tbuilder.DefineMethod("Divide", MethodAttributes.Public | MethodAttributes.Abstract | MethodAttributes.Virtual | MethodAttributes.HideBySig | MethodAttributes.NewSlot, CallingConventions.HasThis, typeof(System.Single), tparams); metDivide.SetImplementationFlags(MethodImplAttributes.Managed); MethodBuilder metSum = tbuilder.DefineMethod("Sum", MethodAttributes.Public | MethodAttributes.Abstract | MethodAttributes.Virtual | MethodAttributes.HideBySig | MethodAttributes.NewSlot, CallingConventions.HasThis, typeof(System.Single), tparams); metSum.SetImplementationFlags(MethodImplAttributes.Managed); MethodBuilder metMultiply = tbuilder.DefineMethod("Multiply", MethodAttributes.Public | MethodAttributes.Abstract | MethodAttributes.Virtual | MethodAttributes.HideBySig | MethodAttributes.NewSlot, CallingConventions.HasThis, typeof(System.Single), tparams); metMultiply.SetImplementationFlags(MethodImplAttributes.Managed); MethodBuilder metSubstract = tbuilder.DefineMethod("Substract", MethodAttributes.Public | MethodAttributes.Abstract | MethodAttributes.Virtual | MethodAttributes.HideBySig | MethodAttributes.NewSlot, CallingConventions.HasThis, typeof(System.Single), tparams); metSubstract.SetImplementationFlags(MethodImplAttributes.Managed); Type tIBuilder = tbuilder.CreateType(); return tIBuilder; }
在上面的代码中,我们首先从 ModuleBuilder.DefineType
创建 TypeBuilder。您应该注意,我以与 MSIL 相同的方式添加了 TypeAttributes
。创建 TypeBuilder 后,我们接下来可以添加方法。DefineMethod 方法通过正确定义 MethodAttributes 来帮助构建方法。CallingConvensions.HasThis 将使该方法成为实例方法。我们还需要具体说明 ReturnType
和参数类型。在本例中,我将 ReturnType 指定为 System.Single
(float),并将参数指定为整数。应该注意的是,我们需要使用 SetImplementationFlags 来指定方法为 cil managed。
既然我们的 IBuilder 接口已经准备就绪,是时候构建我们的实际类型了。
实现 Builder 类
嗯,现在是时候进行最后的修改了。首先,我将创建一个仅包含实现 IBuilder 接口所需方法的基本类,稍后我们将添加委托、事件、新方法、静态方法等。
要创建基本的 Builder 类,我们首先需要一个构造函数。但是,由于我们的构造函数还添加了一些行来初始化属性 FirstNum 和 SecondNum,所以我先定义它们。
1. 实现类型
在 IL 中,大多数类型都是 .class,因为整个主体都依赖于类型头,所以最好从构建类型签名开始。就 IL 而言,在 ILDASM 中它看起来像
.class public auto ansi beforefieldinit EmitMethodSample.Builder extends [mscorlib]System.Object implements EmitMethodSample.IBuilder { }
所以基本上这个类扩展了 System.Object
(对于任何不继承自其他类的类),在我们的例子中它实现了 IBuilder
。
我想,使用 TypeBuilder 构建类型对您来说应该很容易,让我再为您构建一次
Type[] interfaces = { parentBuilder }; TypeBuilder tbuilder = mbuilder.DefineType("Builder", TypeAttributes.Public | TypeAttributes.AutoClass | TypeAttributes.AnsiClass | TypeAttributes.BeforeFieldInit, typeof(System.Object), interfaces);
所以在这里的代码中,我从 parentBuilder
实现了 Builder,它是 IBuilder 接口的 Type 对象。如果你关注代码,你会看到我为类型指定了 BeforeFieldInit,这意味着你可以在不初始化对象的情况下调用静态成员。我还根据 IL 从 System.Object 实现了 Type。
2. 实现字段和属性
现在我们的类型已经准备好了,让我们为类型添加一些字段和属性。如 Builder 类型中所示,我们有两个字段来存储数值,每个字段都使用属性包装器进行包装。让我们看看我到底在说什么
private int firstNum, secondNum; public int FirstNum { get { return this.firstNum; } set { this.firstNum = value; } } public int SecondNum { get { return this.secondNum; } set { this.secondNum = value; } }
所以 FirstNum 和 SecondNum 是我们需要为我们的类型声明的两个属性。现在,如果你回顾 IL 的实现,它看起来像
.field private int32 firstNum .property instance int32 FirstNum() { .set instance void EmitMethodSample.Builder::set_FirstNum(int32) .get instance int32 EmitMethodSample.Builder::get_FirstNum() } .method public hidebysig specialname instance int32 get_FirstNum() cil managed { .custom instance void [mscorlib]System.Diagnostics.DebuggerStepThroughAttribute::.ctor() = ( 01 00 00 00 ) // Code size 12 (0xc) .maxstack 1 .locals init (int32 V_0) IL_0000: nop IL_0001: ldarg.0 IL_0002: ldfld int32 EmitMethodSample.Builder::firstNum IL_0007: stloc.0 IL_0008: br.s IL_000a IL_000a: ldloc.0 IL_000b: ret } .method public hidebysig specialname instance void set_FirstNum(int32 'value') cil managed { // Code size 9 (0x9) .maxstack 8 IL_0000: nop IL_0001: ldarg.0 IL_0002: ldarg.1 IL_0003: stfld int32 EmitMethodSample.Builder::firstNum IL_0008: ret }
因此,考虑到 IL,属性似乎是两个方法的包装器,一个带 get_PropertyName,另一个带 set_PropertyName,其中 get_PropertyName 返回值,set_PropertyName 设置值。所以根据 IL 定义,如果你要实现代码,它看起来像
FieldBuilder fFirst = tbuilder.DefineField("firstNum", typeof(System.Int32), FieldAttributes.Private); PropertyBuilder pFirst = tbuilder.DefineProperty("FirstNum", PropertyAttributes.HasDefault, typeof(System.Int32), null); //Getter MethodBuilder mFirstGet = tbuilder.DefineMethod("get_FirstNum", MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig, typeof(System.Int32), Type.EmptyTypes); ILGenerator firstGetIL = mFirstGet.GetILGenerator(); firstGetIL.Emit(OpCodes.Ldarg_0); firstGetIL.Emit(OpCodes.Ldfld, fFirst); firstGetIL.Emit(OpCodes.Ret); //Setter MethodBuilder mFirstSet = tbuilder.DefineMethod("set_FirstNum", MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig, null, new Type[] { typeof(System.Int32) }); ILGenerator firstSetIL = mFirstSet.GetILGenerator(); firstSetIL.Emit(OpCodes.Ldarg_0); firstSetIL.Emit(OpCodes.Ldarg_1); firstSetIL.Emit(OpCodes.Stfld, fFirst); firstSetIL.Emit(OpCodes.Ret); pFirst.SetGetMethod(mFirstGet); pFirst.SetSetMethod(mFirstSet);
哦,太大了……是的,让我解释一下。首先,我添加了一个字段 firstNum,它是一个数字私有变量。FieldBuilder 可以帮助您向 IL 中添加字段。要定义一个属性,您需要首先定义属性本身,然后您必须定义两个方法,一个用于 Getter,一个用于 Setter,以便 Getter 返回 System.Int32,Setter 接受 System.Int32 作为参数。
操作码提供完整的表达式集。ldarg 加载参数,Ldfld 和 Stfld 将字段加载并设置到字段 fFirst 中。
一个值得一提的好事
所以你现在必须明白,属性实际上保留了带有 get_Property 和 set_Property 的方法,并且它们具有相同的签名。例如,你定义一个如下所示的类
private string first; public string First { get { return this.first; } set { this.first = value; } } public string get_First() { return this.first; } public void set_First(string value) { this.first = value; }
该类将无法编译,因为 get_First 和 set_First 已被保留,编译器会因此抛出警告。

是不是很有趣?
3. 实现构造函数
如果您的类中没有定义构造函数,C# 编译器会自动为您添加一个默认构造函数。它是如何做到的?实际上,对于任何类,System.Object
的默认构造函数都会自动继承到对象中,因此在这种情况下您无需定义默认构造函数。只有当您明确定义默认构造函数时,它才会被写入 IL 中。
在我们的例子中,我明确声明了一个带参数的构造函数,因为向你展示它的代码是很好的。
public Builder(int firstnum, int secondnum) { this.FirstNum = firstnum; this.SecondNum = secondnum; }
为此,让我快速向您展示构造函数在 IL 中的样子。
.method public hidebysig specialname rtspecialname instance void .ctor(int32 firstnum, int32 secondnum) cil managed { // Code size 26 (0x1a) .maxstack 8 IL_0000: ldarg.0 IL_0001: call instance void [mscorlib]System.Object::.ctor() IL_0006: nop IL_0007: nop IL_0008: ldarg.0 IL_0009: ldarg.1 IL_000a: call instance void EmitMethodSample.Builder::set_FirstNum(int32) IL_000f: nop IL_0010: ldarg.0 IL_0011: ldarg.2 IL_0012: call instance void EmitMethodSample.Builder::set_SecondNum(int32) IL_0017: nop IL_0018: nop IL_0019: ret }
要构建一个构造函数,我们需要创建 ConstructorBuilder 对象,你可以从 TypeBuilder 的 DefineConstructor 方法中获取。如果你查看 IL,你会发现 IL 实际上首先调用了 System.Object 的构造函数。这是必需的,因为任何对象都内部继承自基 System.Object。
setFirstNum 和 set_SecondNum 从 IL 调用,用于设置类的 FirstNum 和 SecondNum 的值。
Type[] parameters = { typeof(System.Int32), typeof(System.Int32) }; ConstructorBuilder cBuilder = tbuilder.DefineConstructor(MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName, CallingConventions.Standard, parameters); ConstructorInfo conObj = typeof(object).GetConstructor(new Type[0]); ILGenerator cil = cBuilder.GetILGenerator(); cil.Emit(OpCodes.Ldarg_0); cil.Emit(OpCodes.Call, conObj); cil.Emit(OpCodes.Nop); cil.Emit(OpCodes.Nop); cil.Emit(OpCodes.Ldarg_0); cil.Emit(OpCodes.Ldarg_1); cil.Emit(OpCodes.Call, mFirstSet); cil.Emit(OpCodes.Nop); cil.Emit(OpCodes.Ldarg_0); cil.Emit(OpCodes.Ldarg_1); cil.Emit(OpCodes.Call, mSecondSet); cil.Emit(OpCodes.Nop); cil.Emit(OpCodes.Nop); cil.Emit(OpCodes.Ret);
Constructor 的 MethodAttribute 中的 SpecialName 使该方法对 CLR 特殊。因此,方法名称 ctor 使其成为类的构造函数。
要调用 System.Object 的构造函数,我们需要获取该对象的构造函数。我使用 Reflection 从 Type 获取 ConstructorInfo 并将其传递给 Call OpCode。我们按照 IL 中指定的方式发出代码,然后将生成构造函数。
一件值得记住的有趣事情
关于 Reflection.Emit,有一件有趣的事情需要记住,它在内部向它调用的每个方法发送一个隐藏对象。这就是我们在 C# 中标识为“this”或在 Vb 中标识为“Me”的隐式对象调用。因此,当我们为 OpCodes 调用 Ldarg_0 时,我们实际上指的是作为第一个参数传递给构造函数的隐式对象。因此,我们指定的任何参数都从索引 1 开始。
构造函数与普通方法唯一的区别在于,构造函数不返回值。在 CLR 中,如果收到 OpCodes.Ret,方法会立即返回堆栈中的顶部元素。因此,如果您的堆栈在调用 Ret 之前加载了一个值到堆栈中,当您创建该类型的对象时,您将收到“Invalid Program”异常。因此,在这种情况下,在调用 Ret 之前应调用 Nop 以消耗一个处理周期。
现在我们已经定义了构造函数,让我继续定义方法。
4. 实现 IBuilder 中的方法
现在我们的构造函数已经准备好,是时候实现 IBuilder 对象并为我们定义方法了。随着我们深入代码,我认为您应该清楚如何构建自己的自定义对象。让我们尝试 IBuilder 的 Divide 方法并为我们实现它。
我们之前声明的 Divide 方法看起来像
public float Divide(int firstnum, int secondnum) { try { return firstnum / secondnum; } catch (DivideByZeroException ex) { Console.WriteLine("ZeroDivide exception : {0}", ex.Message); return 0; } }
因此,从这个方法中,您可以理解如何从对象中调用外部成员函数,就像我在这里使用 Console.WriteLine 所做的那样,并且还可以了解如何在代码生成过程中使用 try/Catch 块。所以,废话不多说,让我们再次打开 ILDASM,看看代码有什么不同
.method public hidebysig newslot virtual final instance float32 Divide(int32 firstnum, int32 secondnum) cil managed { // Code size 39 (0x27) .maxstack 2 .locals init (class [mscorlib]System.DivideByZeroException V_0, float32 V_1) IL_0000: nop .try { IL_0001: nop IL_0002: ldarg.1 IL_0003: ldarg.2 IL_0004: div IL_0005: conv.r4 IL_0006: stloc.1 IL_0007: leave.s IL_0024 } // end .try catch [mscorlib]System.DivideByZeroException { IL_0009: stloc.0 IL_000a: nop IL_000b: ldstr "ZeroDivide exception : {0}" IL_0010: ldloc.0 IL_0011: callvirt instance string [mscorlib]System.Exception::get_Message() IL_0016: call void [mscorlib]System.Console::WriteLine(string, object) IL_001b: nop IL_001c: ldc.r4 0.0 IL_0021: stloc.1 IL_0022: leave.s IL_0024 } // end handler IL_0024: nop IL_0025: ldloc.1 IL_0026: ret }
现在,实现表明该方法加载第一个和第二个参数,并使用 Div 操作来除法这些值,Conv.r4 实际上将结果转换为 float32 值。这里使用了声明为 float32 的局部堆栈元素,并且转换后的结果使用 stloc.1 再次推回堆栈。如果一切正常,应用程序将其控制权传递给 IL_0024,导致该方法返回第一个位置的局部堆栈值。
所以让我们使用 Builder 对象来实现同样的功能
MethodBuilder mDivide = tbuilder.DefineMethod("Divide", MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.NewSlot | MethodAttributes.Virtual | MethodAttributes.Final, CallingConventions.Standard, typeof(System.Single), new Type[] { typeof(System.Int32), typeof(System.Int32) }); mDivide.SetImplementationFlags(MethodImplAttributes.Managed); ILGenerator dil = mDivide.GetILGenerator(); dil.Emit(OpCodes.Nop); Label lblTry = dil.BeginExceptionBlock(); dil.Emit(OpCodes.Nop); dil.Emit(OpCodes.Ldarg_1); dil.Emit(OpCodes.Ldarg_2); dil.Emit(OpCodes.Div); dil.Emit(OpCodes.Conv_R4); // Converts to Float32 dil.Emit(OpCodes.Stloc_1); dil.Emit(OpCodes.Leave, lblTry); dil.BeginCatchBlock(typeof(DivideByZeroException)); dil.Emit(OpCodes.Stloc_0); dil.Emit(OpCodes.Nop); dil.Emit(OpCodes.Ldstr, "ZeroDivide exception : {0}"); dil.Emit(OpCodes.Ldloc_0); MethodInfo minfo = typeof(DivideByZeroException).GetMethod("get_Message"); dil.Emit(OpCodes.Callvirt, minfo); MethodInfo wl = typeof(System.Console).GetMethod("WriteLine", new Type[] { typeof(string), typeof(object) }); dil.Emit(OpCodes.Call, wl); dil.Emit(OpCodes.Nop); dil.Emit(OpCodes.Ldc_R4, 0.0); dil.Emit(OpCodes.Stloc_1); dil.Emit(OpCodes.Leave_S, lblTry); dil.EndExceptionBlock(); dil.Emit(OpCodes.Nop); dil.Emit(OpCodes.Ldloc_1); dil.Emit(OpCodes.Ret);
要在 IL 中打开一个 Try 块,您需要使用 BeginExceptionBlock。务必存储标签,以便在需要时可以跳转到特定的 IL 指令代码。现在,在开始使用 BeginCatchBlock 通知 Catch 块之前,我们需要使用 OpCodes.Leave 和 LabelName 离开 Try 块。这将确保应用程序保持作用域。
您可以看到,我们可以有多个 catch 块,并且每个 catch 块都将通过传递给 BeginCatchBlock 的类型来标识。因此,我们只需将字符串加载到 Catch 块中,并调用 Console 的 WriteLine 方法来显示字符串。最后,在调用 EndExceptionBlock 之前,我们再次离开 Try/catch 块。
您应该注意,每当您要调用方法时,都需要使用 MethodInfo 对象。
构建委托
由于为你的类型创建其他方法很简单,所以让我们进一步为你创建一个委托。向你展示如何为类构建委托是个好主意。构建委托与构建其他成员不同。假设我们需要为我们的类型声明一个委托,如下所示
public delegate void BuilderDelegate(string message);
现在看完这一行代码,不要以为 IL 的声明会像这样简单。IL 看起来像
.class auto ansi sealed nested public BuilderDelegate extends [mscorlib]System.MulticastDelegate { .method public hidebysig specialname rtspecialname instance void .ctor(object 'object', native int 'method') runtime managed { } .method public hidebysig newslot virtual instance class [mscorlib]System.IAsyncResult BeginInvoke(string message, class [mscorlib]System.AsyncCallback callback, object 'object') runtime managed { } .method public hidebysig newslot virtual instance void EndInvoke(class [mscorlib]System.IAsyncResult result) runtime managed { } .method public hidebysig newslot virtual instance void Invoke(string message) runtime managed { } }
天哪,委托实际上被定义为实际类型 Builder
内部的一个嵌套类型 (.class),它继承自 System.MulticastDelegate
。它还在新的类型声明内部声明了 Invoke、BeginInvoke 和 EndInvoke 等方法。所以让我们构建这个类型来帮助你,尽管我认为你现在可以自己创建它了。
TypeBuilder tdelegate = tbuilder.DefineNestedType("", TypeAttributes.AutoClass | TypeAttributes.AnsiClass | TypeAttributes.Sealed | TypeAttributes.Public, typeof(System.MulticastDelegate)); MethodBuilder methodBeginInvoke = tdelegate.DefineMethod("BeginInvoke", MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.NewSlot | MethodAttributes.Virtual, typeof(IAsyncResult), new Type[] { typeof(string), typeof(AsyncCallback), typeof(object) }); methodBeginInvoke.SetImplementationFlags(MethodImplAttributes.Runtime | MethodImplAttributes.Managed); MethodBuilder methodEndInvoke = tdelegate.DefineMethod("EndInvoke", MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.NewSlot | MethodAttributes.Virtual,null, new Type[] { typeof(IAsyncResult)}); methodEndInvoke.SetImplementationFlags(MethodImplAttributes.Runtime | MethodImplAttributes.Managed); MethodBuilder methodInvoke = tdelegate.DefineMethod("Invoke", MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.NewSlot | MethodAttributes.Virtual, CallingConventions.Standard, null, new Type[] { typeof(string) });
接下来,我建议您尝试其他方法,您可以使用 EventBuilder 类来构建事件,使用 CustomAttributeBuilder 来构建您自己的用户定义自定义属性等。
总结
现在我们已经实现了所有方法,让我检查一下我是否正确生成了 IL。
//Step 1 : Create the Assembly AssemblyBuilder asmBuilder = this.GetAssemblyBuilder("MyBuilder"); //Step 2: Add A Module to the Assembly ModuleBuilder mbuilder = this.GetModule(asmBuilder); //Step 3: Add the Type IBuilder Type iBuilder = this.CreateIBuilder(mbuilder); //Step 4 : Implement IBuilder to create Builder Type Builder = this.CreateBuilderImpl(mbuilder, iBuilder); dynamic variable = Activator.CreateInstance(Builder, new object[] { 20, 10 }); float result = variable.Sum(30, 40); Console.WriteLine("Result for Sum(30, 40) : {0}", result); result = variable.Substract(50, 25); Console.WriteLine("Result for Substract(50, 25) : {0}", result); result = variable.Multiply(3, 5); Console.WriteLine("Result for Multiply(3, 5) : {0}", result); result = variable.Divide(30, 5); Console.WriteLine("Result for Divide(30, 5) : {0}", result);
您应该注意,我使用 dynamic 来避免再次不必要地使用 Reflection 类。
所以它看起来不错,编译后我得到了这样的输出
因此,生成的 IL 对我们来说效果很好。您还可以使用以下命令将 IL 保存为程序集
asmBuilder.Save("MyBuilder.dll");
处理无效 IL
在构建 IL 时,您可能会经常遇到 IL 无法编译的情况。

别担心,.NET 附带了一个免费工具,随 Visual Studio 一起安装,可以帮助您摆脱这些困境。常见的异常类型是“通用语言运行时检测到无效程序”。该工具名为 PeVerify.exe。
安装 Visual Studio 后,您可以打开控制台并尝试调用以下语句
peverify.exe <assemblypath>\yourassembly.dll
再次编译程序集后,它将给出在构建 Type 时发生的实际异常。您可以从以下位置阅读有关 PEVerify 的更多信息:MSDN PEVerify 参考 [^]参考文献
互联网上有很多关于这个主题的参考资料,可能会对您有所帮助。让我们为您列举一些
- Reflection.Emit 演练 [^]
- 动态代码生成 [^]
- 反射第二部分:Emit [^]
历史
初始发布 - 2010年10月26日结论
为您创作这篇文章很有趣。我也很兴奋能为您写一篇这样的文章。对我来说这一切都是新的,但我尽力写得尽可能多。我希望您会喜欢我的帖子。
如果你认为我犯了任何错误,请告诉我,因为我在这方面并非大师,这样我们就可以使这篇文章更好。感谢您阅读这篇文章。