65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 Reflection.Emit 创建动态类型的介绍

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.88/5 (32投票s)

2006年3月7日

CPOL

25分钟阅读

viewsIcon

236323

动态类型可以使用 Reflection.Emit 创建。

引言

动态类型可以为框架开发者提供高效的编程抽象,而不会像许多抽象那样通常会带来性能损失。通过针对接口编程并使用工厂设计模式,您可以开发一个具有抽象的通用优势,但同时也具有硬编码逻辑的性能优势的框架。

动态类型工厂使用程序的底层元数据来确定在运行时“构建”新类的最佳方式。此类的代码直接“发出”到内存中的程序集,并且无需通过 .NET 语言特定的编译器运行。一旦类被发出,它就会被 CLR“烘焙”,并准备好供您的应用程序使用。

这种模式允许您创建具有硬编码逻辑的非常特定的类,但也可以很灵活,因为您可以根据需要发出尽可能多的类,只要所有类都使用相同的公共接口。

借助 .NET 中的反射,现在您可以编写数千种后期绑定抽象,以创建通用的多用途函数或框架。这些抽象对企业开发人员来说非常有价值,因为它们可以显著缩短开发时间。当您可以编写一次通用模式并使其适用于每种情况时,为什么还要为十个共享共同模式的不同类编写十次逻辑的细微变体呢?

这些后期绑定抽象的许多问题在于它们通常会带来性能损失。这就是 System.Reflection.Emit 命名空间(以下简称 Reflection.Emit)和动态类型可以发挥巨大作用的地方。本文是两部分系列的第一部分,我将讨论什么是动态类型,编写和使用它们时要使用的策略,以及如何创建它们。我将介绍动态类型的一种可能用法,并提供一些代码。但我会将实现动态类型的完整示例和完整代码留到本文的第二部分。

动态类型的可能用途

使用动态类型最常见的原因是解决性能问题。作为程序员,我多次遇到的一种常见模式是对象/关系数据库映射框架,其目的是提供一个通用的 API,用于将类属性映射到数据库表或存储过程结果集。大多数使用某种元数据(例如 XML)来映射结果集中的哪些列写入到类的哪个属性。为了做到这一点,它们使用反射查询类以获取所需的属性,并再次使用反射来用结果集中的数据填充属性。

这创建了一个框架,允许您快速轻松地添加新类,代码量大大减少。但是使用反射会严重影响性能。相反,您可以创建一个 O/R 映射框架,该框架创建一个动态类型,该类型具有特定于该类和用于填充它的列的硬编码映射逻辑。

什么是动态类型

动态类型是在运行时,在程序内部手动生成并插入到 AppDomain 中的类型或类。动态类型的酷之处在于程序可以评估一组给定的元数据并创建一个针对当前情况进行优化的类型。为此,您使用 Reflection.Emit 命名空间提供的类来创建新类型,并将函数直接发出到其中。

使用 Reflection.Emit 创建动态类型的缺点是您不能简单地将 C# 代码转储到动态程序集中并让 C# 编译器将其编译为 IL。那会太容易了。您必须使用 Reflection.Emit 中的类来定义和生成 Type、Method、Constructor 和 Property 定义,然后将 IL 操作码直接插入或“发出”到这些定义中。这比正常的编码更难,因为您必须使用并普遍理解 IL。但也没那么糟糕。有一些方法可以使这个学习曲线变得更容易,我稍后会介绍。

定义通用接口

使用 Reflection.Emit 创建类型有一个主要问题,而且相当严重。当您使用动态类型开发应用程序时,没有可供编程的 API。想一想。您将在运行时生成的类在设计时不存在。那么,您应该如何针对动态类型进行编程呢?啊,接口的力量。

因此,第一步是确定动态类型的公共接口将是什么。让我们看一个例子。前面我提到了一个对象/关系数据库映射框架,它可以将数据库中的列映射到应用程序中的对象。您可以为应用程序中的每个类创建一个映射函数,或者您可以创建一个框架,该框架根据它正在加载的类来知道哪些列分配给哪个属性。

所以我为这种类型的框架想出的接口可能看起来像这样

public interface IORMapper
{
    void PopulateObject(object entity, DataRow data);
}

这看起来并不那么令人兴奋,但您的动态类型生成器可以读取 XML 文件,并使用其内容创建一个新类型,该类型接受输入的实体对象,将其转换为适当的类型,然后将 DataRow 中的列分配给实体对象中适当的属性。然后,这个新的动态类型可以用于填充该类型的每个新对象。

市面上是否已经存在其他 OR 映射框架?是的,但大多数都利用反射或后期绑定来映射和分配哪个列分配给哪个属性,正如我所说,反射是性能最差的杀手之一。

动态类型工厂

接下来要做的是设计一个类,该类将生成动态类型并将其返回给调用者。对于动态类型生成器,工厂模式非常适用。工厂模式通常用于开发人员希望隐藏如何创建类型新实例的细节。当您有一个抽象基类或接口以及几个继承自该基类或接口的类时,通常会使用这种模式。类型的消费者不应该手动创建类型的新实例,因此他们必须调用一个工厂方法,该方法确定要创建哪个类并将其返回给消费者。这是一种隐藏类型初始化逻辑的绝佳黑盒方式,因此它不会在应用程序中重复。这完全符合我们解决方案的需求,因为调用者无法显式调用动态类型的构造函数。此外,我想将动态类型的实现细节隐藏起来,不让调用者知道。所以工厂类的公共 API 将像这样

public class ORMapperFactory
{
    public static IORMapper CreateORMapper(Type mappedType)
    {
                //method implementation
    }
    //private factory methods
}

在此 O/R 映射框架中,函数 CreateORMapper 将获取传入类型的名称,然后在映射 XML 文件中查找与该类型名称匹配的节点。此 XML 节点将包含一个内部节点集合,告诉工厂哪些列名映射到哪些对象属性。当工厂生成动态类型时,它将使用此 XML 元数据生成 IL 代码,将输入对象强制转换为为此映射器创建的类型,然后创建代码以将 DataRow 列中的值分配给特定的对象属性。差不多就是这样了。

一旦生成了这个动态类型,它就可以用于任何需要从 DataRow 填充的该类型的新对象。这样,应用程序只需承担一次类型生成的成本。

以下序列图大致演示了其工作原理。首先,Consumer 类调用 ORMapperFactory 并请求一个 IORMapper 实例。Consumer 传入类型“typeof(Customer)”,工厂将使用该类型来确定它需要生成和返回哪个 ORMapper。然后,Consumer 调用新生成的 ORMapper 实例,传入一个 DataRow 和一个为空的实例,该实例是为此映射器生成的类型,在本例中是 Customer 类。ORMapper 具有硬编码逻辑,可将 DataRow 中的数据分配给 Customer 类的正确属性。然后,Consumer 可以自由地调用 Customer 类的属性并获取其值。

Dynamic Type Factory Sequence Diagram

设置动态类型

在开始展示如何实际将 IL 发送到 IORMapper.PopulateObject() 方法之前,您需要先完成一些清理任务。首先,您需要设置一个程序集来保存新类型。由于 Reflection.Emit 无法将新类型添加到现有程序集中,因此您必须在内存中生成一个全新的程序集。为此,您可以使用 AssemblyBuilder 类。

private static AssemblyBuilder asmBuilder = null;
private static ModuleBuilder modBuilder = null;

private static void GenerateAssemblyAndModule()
{
    if (asmBuilder == null)
    {
        AssemblyName assemblyName = new AssemblyName();
        assemblyName.Name = "DynamicORMapper";
        AppDomain thisDomain = Thread.GetDomain();
        asmBuilder = thisDomain.DefineDynamicAssembly(assemblyName, 
                     AssemblyBuilderAccess.Run);
 
        modBuilder = assBuilder.DefineDynamicModule(
                     asmBuilder.GetName().Name, false);
    }
}

要创建新的 AssemblyBuilder 实例,您需要从 AssemblyName 实例开始。创建一个新实例,并将其指定为您要调用的程序集名称。然后从静态 Thread.GetDomain() 方法获取 AppDomain。此 AppDomain 实例将允许您使用 DefineDynamicAssembly() 方法创建新的动态程序集。只需传入 AssemblyName 实例和 AssemblyBuilderAccess 的枚举值。在这种情况下,我不想将此程序集保存到文件中,但如果我想这样做,我可以使用 AssemblyBuilderAccess.SaveAssemblyBuilderAccess.RunAndSave

创建 AssemblyBuilder 后,还需要创建 ModuleBuilder 实例,该实例稍后将用于创建新的动态类型。使用 AssemblyBuilder.DefineDynamicModule() 方法创建新实例。如果需要,您可以为动态程序集创建任意数量的模块,但在这种情况下,只需要一个。

幸运的是,一旦创建了 AssemblyBuilderModuleBuilder,就可以反复使用相同的实例来创建所需数量的新动态类型,因此只需创建一次。

接下来,为了创建实际的动态类型,您必须创建一个新的 TypeBuilder 实例。以下代码将创建一个新类型并将其分配给您的动态程序集

private static TypeBuilder CreateType(ModuleBuilder modBuilder, string typeName)
{
    TypeBuilder typeBuilder = modBuilder.DefineType(typeName, 
                TypeAttributes.Public | 
                TypeAttributes.Class |
                TypeAttributes.AutoClass | 
                TypeAttributes.AnsiClass | 
                TypeAttributes.BeforeFieldInit | 
                TypeAttributes.AutoLayout, 
                typeof(object), 
                new Type[] {typeof(IORMapper)});

    return typeBuilder;
}

您通过调用 ModuleBuilder.DefineType() 方法来创建 TypeBuilder 类的实例,将类名作为第一个参数,将定义动态类型所有特征的 TypeAttributes 枚举值作为第二个参数。第三个参数是动态类型继承自的类的 Type 实例,在本例中是 System.Object。第四个参数是动态类型将继承自的接口数组。这在解决方案中非常重要,所以我需要传入 IORMapper 的类型。

我想在这里指出一件事。你有没有注意到创建这些 Reflection.Emit 类实例的模式?AppDomain 用于创建 AssemblyBuilderAssemblyBuilder 用于创建 ModuleBuilderModuleBuilder 用于创建 TypeBuilder?这是工厂模式的另一个例子,这是 ReflectionEmit 命名空间中的一个常见主题。你能猜到如何创建 MethodBuilderConstructorBuilderFieldBuilderPropertyBuilder 类吗?当然是通过 TypeBuilder

但我不想学习 IL!

好的,现在是时候深入研究 Reflection.Emit 类并创建一些 IL 了。但是,如果您真的想(或需要)使用动态类型,但又不想花几周时间仔细阅读 IL 规范和其他文档来学习 IL 怎么办?没问题。微软为我们提供了一个随 Visual Studio .NET 提供的工具,它将为您提供一个巨大的领先优势:ILDasm.exe

ILDasm 允许您检查程序集的内部结构,最值得注意的是构成程序集的元数据和 IL 代码。这就是 ILDasm 在创建动态类型时为您提供巨大帮助的地方。您无需尝试找出需要为动态类型生成哪些 IL 代码,只需在 C# 中原型化动态类型,将其编译成程序集,然后使用 ILDasm 转储 IL 代码。之后,只需弄清楚 IL 的含义,然后尝试使用 Reflection.Emit 中可用的类重新创建它即可。ILDasm 在我学习创建动态类型的来龙去脉方面提供了巨大的帮助。

现在,说您不需要了解或理解 IL 就可以创建动态类型是真实的。但我可以说,如果您对基本的 IL 语法和操作码以及基于堆栈的编程如何工作有一般的了解,那将非常有帮助。我甚至不会尝试涵盖这一点,但在本文末尾,我列出了一些在学习 IL 方面对我非常有帮助的读物。

使用 Reflection.Emit 发射方法的剖析

无论您是想编写方法、构造函数还是属性,本质上您都在编写方法;一段执行某个功能的代码块。而且,在使用 Reflection.Emit 定义这些类型的代码构造时,需要注意一些事项。

在 C# 中,如果某个类型的默认构造函数没有任何功能,则不需要在类中定义它。C# 编译器会为您处理此问题。IL 和 Reflection.Emit 中也是如此;它都由 ilasm.exeTypeBuilder.CreateType() 方法在幕后为您处理。在这个例子中,我没有要添加到构造函数中的任何功能,但我仍然要定义它,因为它是一个很好的、简单的示例方法。

现在已经创建了一个 TypeBuilder 实例,下一步是创建 ConstructorBuilder 类的一个实例。TypeBuilder.DefineConstructor() 方法接受三个参数:一个 MethodAttribute 枚举、一个 CallingConvention 枚举和一个与构造函数输入参数列表对应的 Type 数组。如下所示

ConstructorBuilder constructor = typeBuilder.DefineConstructor(
        MethodAttributes.Public | 
        MethodAttributes.SpecialName | 
        MethodAttributes.RTSpecialName, 
        CallingConventions.Standard, 
        Type.EmptyTypes);

那么,我怎么知道在定义构造函数时如何使用 SpecialNameRTSpecialName 呢?我不知道。我作弊了,偷看了一下 ILDasm,看看 C# 编译器是如何创建构造函数的。

以下是 C# 编译器在定义默认构造函数时创建的 IL

.method public specialname rtspecialname 
        instance void .ctor() cil managed
{
    .maxstack  2
    IL_0000:  ldarg.0
    IL_0001:  call  
              instance void 
               [mscorlib]System.Object::.ctor()
    IL_0006:  ret
}

请注意,在我的 Reflection.Emit 代码中,我没有定义 MethodAttributes.Instance 值,但 IL 代码为构造函数定义分配了“instance”属性。这是因为 MethodAttributes 枚举没有该值。相反,它有一个 MethodAttributes.Static 值。如果未设置 Static 值,则 ConstructorBuilder 会隐式为您设置“instance”属性。

我传入 DefineConstructor() 方法的下一个值是 CallingConventions.Standard。MSDN 文档中关于 CallingConventions 枚举的不同值的信息很少。但据我所知,如果您传入 Standard,CLR 会为您决定合适的 CallingConvention。所以,我总是默认使用这个值。

传入 DefineConstructor 的最后一个值是一个 Type 数组。所有函数构建器都接受此参数,它对应于正在定义的方法中传入的每个参数的 Type。此数组中类型的顺序必须与方法参数列表中的顺序匹配。由于默认构造函数不接受任何参数,因此可以使用预定义的 Type.EmptyType,它是一个空的 Type 数组。

所以,这是创建构造函数的整个方法

private static void CreateConstructor(TypeBuilder typeBuilder)
{
    ConstructorBuilder constructor = typeBuilder.DefineConstructor(
                        MethodAttributes.Public | 
                        MethodAttributes.SpecialName | 
                        MethodAttributes.RTSpecialName, 
                        CallingConventions.Standard, 
                        new Type[0]);
    //Define the reflection ConstructorInfor for System.Object
    ConstructorInfo conObj = typeof(object).GetConstructor(new Type[0]);
 
    //call constructor of base object
    ILGenerator il = constructor.GetILGenerator();
    il.Emit(OpCodes.Ldarg_0);
    il.Emit(OpCodes.Call, conObj);
    il.Emit(OpCodes.Ret);
}

关于 IL 有趣的一点是,你没有太多免费的东西。在 C# 中,即使每个类在继承链中的某个地方都继承自 System.Object,你实际上也不必调用 Object 的基构造函数(尽管如果你愿意,也可以调用)。但在 IL 中,你必须为你自己的类调用基类构造函数,在这个例子中,你使用 System.Object 默认构造函数。

为了使用 Reflection.Emit 来做到这一点,您需要从 ILGenerator 实例开始。这个类是您使用动态类型进行大部分工作的核心。ILGenerator 有一个 Emit() 方法,用于实际将 IL 操作码注入您的新方法。ILGenerator 实例是从您当前正在使用的“构建器”对象(ConstructorBuilderMethodBuilderPropertyBuilder 等)创建的。Emit() 方法有 17 个重载,所以我不会尝试逐一介绍。但是,每个重载都将 OpCodes 类静态属性之一的值作为其第一个参数。第二个参数对应于所需的任何 IL 操作码参数(如果有)。

在 IL 和 Reflection.Emit 中创建实例方法时,要记住的另一个重要事项是,每个方法都会传入一个隐藏参数。这个隐藏参数始终是第一个输入参数,它是一个指向该方法所属对象的引用。这就是您能够在 C# 中使用“this”关键字,或在 VB.NET 中使用“Me”关键字的方式。这是一个重要的事实,因为任何时候您需要从类内部调用实例方法或字段,都必须使用此参数来引用调用类型的实例。关于 IL 的另一个有趣之处在于,参数总是通过它们在参数列表中的位置索引来引用。因此,我刚刚提到的隐藏参数总是被称为参数 0。任何显式定义的方法参数都从索引 1 开始引用。这意味着每个实例方法至少有一个参数,即使是默认构造函数和属性 getter 也是如此。那么静态方法呢?静态方法没有这个隐藏参数,因此显式定义的方法参数应该从索引 0 开始引用。

这里是 IL 和 Reflection.Emit 之间另一个很大的不同。请注意上面 IL 代码的 IL_0001 行,您使用“call”操作码并传入“instance void [mscorlib]System.Object::.ctor()”。这基本上意味着调用 System.Object 的实例构造函数。要使用 Reflection.Emit 执行此操作,您需要使用反射并创建一个与 System.Object 构造函数对应的 ConstructorInfo 实例。然后,您将此实例作为第二个参数传入 Emit() 方法。

所以,现在所有这些都已解决,让我们来看看默认构造函数做了什么。正如我之前提到的,我将假设您在阅读本文时了解 IL 和基于堆栈的编程的基础知识。为了调用 Object 的构造函数,您首先需要将隐藏的“this”参数添加到堆栈中。然后使用“call”操作码和 ConstructorInfo 实例。这相当于调用“this.base();”,这在 C# 中是违法的,如果该类直接继承自 System.Object,但在 IL 中是必需的。最后,使用返回操作码告诉线程离开构造函数,这是每个方法结束时必需的。

现在,构造函数就像任何其他方法一样,只是它不能有返回值。在 IL 中,当调用 ret 操作码时,CLR 将获取堆栈顶部的任何值并尝试返回它。如果您在堆栈中留下一个值,这在构造函数中可能会成为问题。如果在调用返回时堆栈不为空,您将收到神秘的“Common Language Runtime detected an invalid program”错误。更糟糕的是,您甚至要等到运行应用程序并实际尝试通过调用其构造函数来创建动态类型的实例时才会收到此错误。(要在运行之前验证 IL 代码的语法是否正确,请参阅文章末尾的“我如何知道我的 IL 是否正确?”部分)。事实上,如果在调用 ret 操作码时堆栈上加载了多个值,您将收到相同的错误。要测试此情况,请在 ret 操作码之前放置以下代码

il.Emit(OpCodes.Ldc_I4_3);
//il.Emit(OpCodes.Pop);

运行一个测试程序,看看会发生什么。它崩溃了,对吧?第一行代码将一个常量 Int32 值 3 加载到堆栈上。当调用 ret 时,CLR 看到堆栈上有 3,但您的方法的返回类型是 void。这是非法的,因此它会抛出异常。现在,取消注释下一条语句。pop 操作码的作用是从堆栈中移除顶部值。现在,当 ret 操作码执行时,您的构造函数堆栈中没有任何内容,并且它可以成功返回。

在为动态类型创建其他构造函数、函数和属性时,可以遵循这个基本结构。我不会介绍实际将 DataRow 列映射到对象属性的主函数,因为它相当重复,而且我已涵盖了所需的基础知识。重要的是要记住,使用 Reflection.Emit 创建函数的最简单方法是先在 C# 中进行原型设计,然后使用 ILDasm 转储 IL。之后,您只需创建一个 ILGenerator 并使用 Emit() 方法将 IL 发射到您的动态类型中,其结构与 ILDasm 向您展示的完全相同。

创建和使用动态类型的实例

既然所有创建动态类型的工具都已到位,我还有最后一个领域要介绍:工厂类如何实际创建一个新的动态类型并向调用者返回一个新实例,以及如何使用动态类型。下面是工厂类的基本结构

TypeBuilder typeBuilder = null;
public static IORMapper CreateORMapper(Type mappedType)
{
    //check to see if type is already created
    if (typeBuilder == null)
    {
        //Didnt exist, so create assembly and module 
        GenerateAssemblyAndModule();                                  
 
        //create new type for table name
        TypeBuilder typeBuilder = CreateType(modBuilder, 
        "ORMapper_" + mappedType.Name);
        //create constructor
        CreateConstructor(typeBuilder);
         
        //create O/R populate object
        CreatePopulateObject(typeBuilder, mappedType);
    }
 
    Type mapperType = typeBuilder.CreateType();
 
    try
    {
        IORMapper mapper = (IORMapper)mapperType.GetConstructor(
                            Type.EmptyTypes).Invoke(Type.EmptyTypes);
    }
    catch (Exception ex)
    {
        //Log error if desired
        return null;
    }
    return mapper;
}

工厂所做的第一件事是检查 TypeBuilder 类是否已创建。如果尚未创建,工厂将调用我已介绍过的私有方法,这些方法会为动态类型创建 DynamicAssemblyDynamicModuleTypeBuilder、构造函数和 PopulateObject() 方法。一旦这些步骤完成,它将使用 TypeBuilder.CreateType() 方法返回 ObjectMapperType 实例。从这个 Type 实例中,我可以调用 CreateConstructor(),并调用构造函数以实际创建动态类型的工作实例。

这是关键时刻。创建新类型并调用该类型的构造函数将调用之前构建的构造函数。如果任何 IL 发射错误,CLR 将抛出异常。如果一切正常,您将拥有 ObjectMapper_<TypeName> 的工作副本。工厂获得 ObjectMapper_<TypeName> 的实例后,它将向下转换并返回 IORMapper

使用动态类型非常简单。要记住的主要事情是您必须针对接口进行编码,因为在设计时,该类不存在。下面是调用 ObjectMapperFactory 类并请求 IORMapper 实例的示例代码。如果这是第一次调用工厂,它将生成 ORMapper 并返回其一个实例。从那时起,无论何时调用工厂,它都不必生成类型,只需创建一个实例并返回它即可。消费者可以调用 PopulateObject() 并传入 DataRowCustomer 类的一个空实例。如下所示

IORMapper mapper = 
     ObjectMapperFactory.CreateObjectMapper(typeof(Customer));
foreach (DataRow row in data.Rows)
{
    Customer c = new Customer();
    mapper.PopulateObject(row, c);
    customers.Add(c);
}

您也可以让 ORMapper 充当工厂,只需传入一个 DataRow,并让 PopulateObject 方法创建 Customer 的新实例,然后用数据填充它。

我如何知道我的 IL 是否正确?

好的,现在我完成了动态类型工厂,我在 Visual Studio 中编译了解决方案,并且没有错误。如果我运行它,它会工作,对吗?也许。Reflection.Emit 的缺点是您可以发出几乎任何您想要的 IL 组合。但是没有设计时编译器检查您编写的内容是否是有效的 IL。有时,当您使用 TypeBuilder.CreateType() “烘焙”您的类型时,如果出现问题,它会抛出错误,但仅限于某些问题。有时,您直到第一次尝试调用方法时才会收到错误。还记得 JIT 编译器吗?JIT 编译器直到第一次调用方法时才会尝试编译和验证您的 IL。因此,很有可能,而且实际上很可能,您直到实际运行应用程序、生成类型并首次调用动态类型时才会发现您的 IL 无效。但是 CLR 会给出有用的错误,对吗?不太可能。通常,我都会收到始终有用的“Common Language Runtime detected an invalid program”异常。

好的,那么您如何判断您的动态类型是否包含有效的 IL 呢?PEVerify.exe 来救援!PEVerify 是一个随 .NET 提供的工具,它将检查程序集是否包含有效的 IL 代码、结构和元数据。但是,要使用 PEVerify,您必须将动态程序集保存到物理文件中(请记住,到目前为止,动态程序集只存在于内存中)。要为动态程序集创建实际文件,您需要对工厂代码进行一些更改。首先,将 AppDomain.DefineDynamicAssembly() 方法中的最后一个参数从 AssemblyBuilderAccess.Run 更改为 AssemblyBuilderAccess.RunAndSave。其次,更改 AssemblyBuilder.DefineDynamicModule() 以将其第二个参数作为程序集文件名传入。最后,添加一行新代码以将程序集保存到文件。我将其放在调用 TypeBuilder.CreateType() 之后,如下所示

Type draType = typeBuilder.CreateType();
assBuilder.Save(assBuilder.GetName().Name + ".dll");

现在这些都已到位,运行应用程序并创建动态类型。运行后,您应该在解决方案的 Debug 文件夹中有一个名为“DynamicObjectMapper.dll”的新 DLL(假设您使用的是 Debug 版本)。现在,打开 .NET 命令提示符窗口,键入“PEVerify <程序集路径>\ DynamicObjectMapper.dll”并按 Enter。PEVerify 将验证程序集,并告诉您一切正常或哪里出了问题。PEVerify 的好处在于它会提供关于哪里出了问题以及在哪里找到问题的详细信息。另请注意,仅仅因为 PEVerify 出现错误并不意味着程序集无法运行。例如,当我第一次编写工厂类时,我在调用静态 String.Equals() 方法时使用了“callvirt”操作码。这导致 PEVerify 输出错误,但它仍然运行。这是一个简单的修复,可以使用“call”操作码调用静态方法,下次我运行 PEVerify 时,它没有发现任何错误。

最后一点,如果您更改代码以输出物理文件,请务必将其改回原来的样子。这是因为,一旦动态程序集保存到文件,它就会被锁定,您将无法添加任何新的动态类型。在这种情况下,这会成为一个问题,因为您的应用程序可能需要根据几种不同的类型创建多个对象映射器。但是,在生成第一个类型后,如果您将程序集保存到文件,则会抛出异常。

Reflection.Emit 在 .NET Framework 2.0 中的未来

那么,.NET 2.0 中有什么新功能呢?嗯,一个很棒的新功能叫做轻量级代码生成 (LCG)。LCG 提供了快速将全局静态方法创建到程序集中的方法。但最酷的部分在这里。您不必创建动态程序集、动态模块和动态类型来将方法发出到其中。您可以将方法直接发出到主应用程序程序集中!只需使用其六个构造函数重载之一创建新的 DynamicMethod 类(不需要工厂方法)。接下来,创建您的 ILGenerator 并发出您的 IL 操作码。然后,要调用该方法,您可以使用 DynamicMethod.Invoke() 方法,或者使用 DynamicMethod.CreateDelegate() 方法获取指向动态方法的委托实例,然后随意调用该委托。

LCG 似乎与 .NET 2.0 中的另一个新功能匿名方法非常相似。匿名方法是不属于任何类的全局静态方法,并作为委托公开和调用。如果 DynamicMethod 类在幕后只是在程序集中创建了一个匿名方法,我不会感到惊讶,特别是考虑到 DynamicMethod 公开了一个返回委托的方法。

本文介绍的解决方案同样可以通过新的 DynamicMethod 类实现。由于它是一个只有一个公共方法的类,因此它非常适合 LCG。它只需要对工厂方法进行一些重组。工厂不再创建 IDataRowAdapter 的实例并将其传回给用户,您可以在设计时定义一个名为 DataRowAdapter 的类。它有一个类型为 delegate 的私有变量,公共的 GetOrdinal() 方法只是调用委托上的 Invoke() 方法并返回值。工厂可以创建一个新的 DataRowAdapter 实例,创建 DynamicMethod,从 DynamicMethod 获取委托,并将其存储在 DataRowAdapter 中。当用户调用 GetOrdinal 时,委托将被调用并返回整数序数。

Reflection.Emit 命名空间的另一个主要补充是创建动态类型时对泛型类型的完全支持。

本主题的进一步阅读

要了解 IL 的良好介绍,请查阅 Simon Robinson 的书籍《Expert .NET 1.1 Programming》的前几章(这通常是一本很棒的 .NET 书籍)。接下来,我建议阅读 Jason Bock 的书籍《CIL Programming: Under the Hood of .NET》。整本书都是关于 IL 编程的,他有几章关于创建动态类型,还有一章关于调试 IL 和动态类型。最后,如果您想要 IL 的“黑宝书”,请购买 Serge Lindin 的《Inside Microsoft .NET IL Assembler》。这本书相当枯燥,但内容无可置疑。Serge Lindin 正在更新这本书的第二版,该版本将涵盖 2.0 内容,并计划于 5 月发布。

© . All rights reserved.