引言
动态类型(Dynamic Types)可以向框架开发者提供高效的抽象编程模型,而不会产生通常因抽象而导致的性能损失。通过对面向接口编程和工厂设计模式的使用,可以开发一个框架,它既享有抽象编程模型通用的好处,同时也兼具硬编码逻辑的性能优势。
动态类型工厂使用程序基本元数据,确定以最佳的方式在运行时建立新类型。类代码被直接“发出”到内存中的程序集中,无需通过.NET语言特定的编译器编译。类一旦被“发出”,它就已经被CLR“烘烤”好并随时可供应用程序使用了。
这种方式看似只允许我们创建具有硬编码逻辑的类,但其实是非常灵活的,因为你可以发出很多类,只要让所有的类实现相同的接口即可。
通过.NET中的反射,你可以写成千上万的后期绑定抽象来建立通用的功能和框架。这些抽象对于企业开发者非常有价值,因为可以大大缩短开发时间。当写一个通用模式就可以应用到所有的情况时,为什么还要把它的逻辑在十个不同的类中写十次呢?
但是这些后期绑定抽象编程的应用程序通常会遭遇性能问题,这就是System.Reflection.Emit命名空间(以下简称Reflection.Emit)和动态类型有很大用途的地方。本文是由两个部分组成的系列文章的第一部分,我会介绍什么是动态类型以及它的用途,实现和使用以及如何创建。我将会尽可能地阐述动态类型的用法并且给出示例代码,完整的代码包括动态类型的实现将在本系列文章的第二部分给出。
动态类型可能的用途
使用动态类型最常见的原因就是解决性能问题。作为一个程序员,ORM框架是我多次遇到的常见的模式,它为映射类的属性到数据库表或存储过程结果集提供通用的接口。大多数时候我们会使用某种形式的元数据比如XML将结果集中的列映射到类的属性;为了做到这点,他们通过反射来查询类所需的属性,再使用反射将结果集的数据填充到类的属性中。
这样创建的ORM框架允许我们以更少的代码简单快速地添加新类,但是使用反射会导致严重的性能问题,相反,你可以使用动态类型创建一个ORM框架,由该动态类型创建类和用以填充数据的列之间的硬编码映射逻辑。
究竟什么是动态类型
动态类型是在运行时手动生成的类或类型,并插入程序所在的AppDomain中。动态类型很酷的一点就是能评估一组给定的元数据新建一个根据当前情况进行优化过的类型,要做到这一点,就要使用Reflection.Emit命名空间提供的类创建新类型并将其功能直接“发出”。
使用Reflection.Emit创建动态类型的缺点是,不能直接将你的C#代码直接“导入”动态程序集,然后通过C#编译器编译成IL中间码,否则那就太简单了。你必须使用Reflection.Emit提供的类来定义和生成类型,方法,构造器和属性,然后插入或者“发出”IL操作码到这些定义中。这会比正常编码要困难一些,因为你必须使用IL中间码,并且对它有大体的了解,但是话又说回来,也没有想象的那么困难,后面我就会讲到,有很多途径可以让它变得简单。
定义接口
使用Reflection.Emit进行类型创建是我们面临主要的问题,而且是一个很大的问题。但是在使用动态类型开发应用程序时,却发现没有相应的API对其进行编程,想想吧,在运行时生成的类在设计时并不存在,那么针对编写针对动态类型的程序呢,没错,接口的力量!
所以,第一步,先弄清楚动态类型的公共接口应该如何定义。让我们来看一个例子。我之前提到的ORM框架,它将数据库的字段映射到程序中的对象,你可以为每一个类建立一个映射功能,或者可以开发一个框架,根据它加载的类确定哪些字段应该赋值到哪些属性。
所以我想这个接口应该是这样的:
public interface IORMapper { void PopulateObject(object entity, DataRow data); }
动态类型生成器读取XML文件内容创建新类型,并使用输入的实体对象将新类型转换为对应的类型,然后从DataRow中读取相应列的数据赋值给实体对象的对应属性,这样动态类型就可以用来将数据填充到该类型的每一个对象中了。
是否已经存在这样的ORM框架了呢?答案是肯定的,但是大多数ORM框架采用反射或者后期绑定策略完成数据库列和对象属性之间的映射,正如我之前所说,反射是最厉害的性能杀手之一。
动态类型工厂
接下来要做的就是设计用于生成的动态类型的类,它返回创建好的动态类型给调用者。对于动态类型生成器,工厂模式再合适不过了。当开发者想要隐藏如何创建一个实例的细节时,一般会选择工厂模式,此模式通常用于这种情况,存在一个抽象基类或接口,并且有多个类继承该基类或接口,而该类型的消费者不应该手动创建该类型的实例,所以必须调用工厂方法以决定创建哪个类的实例返回给消费者。非常棒的一个黑盒子,它隐藏了类型初始化逻辑,并且对于整个程序都没有重复,这正是我们需要的,因为调用者无法显式调用动态类型的构造器。而且我希望对调用者隐藏动态类型的实现细节,所以工厂类接口的代码如下:
public class ORMapperFactory { public static IORMapper CreateORMapper(Type mappedType) { //method implementation } //private factory methods }
在我们的ORM框架中,CreateORMapper方法接受一个Type类型的参数,然后在XML映射文件中查找与该类型匹配的节点,该节点拥有一个内部节点集,它包含了所有列与对象属性之间的映射关系。当工厂生成动态类型时,就会使用XML元数据来生成IL中间码将输入对象强制类型转化为Mapper需要创建的类型,最后编码将数据从DataRow中的列赋值给指定的属性,就是这样。
一旦生成了动态类型,所有需要从DataRow中获取数据并赋值到该类型的对象都可以使用它,并且应用程序只需花费生成一次该类型的开销。
下面的序列图展示了它是如何工作的。首先,Consumer类调用ORMapperFactory请求IORMapper实例,然后将实参typeof(Customer)传入,工厂类会通过它来决定ORMapper创建哪种类型的对象返回。然后Consumer调用新生成的ORMapper实例,并将其传递给Customer和DataRow。ORMapper通过硬编码将数据从DataRow填充到Customer类的对应的属性,然后Consumer就可以使用Customer类的属性和值了。
建立动态类型
了解如何将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实例,该实例允许用DefineDynamicAssembly方法以指定名称和访问模式定义动态程序集,只需要向其传入AssemblyName的实例和AssemblyBuilderAccess的枚举值即可,这里我不想把程序集保存到文件中,如果想保存的话,只需使用枚举值AssemblyBuilderAcess.Save或者AssemblyBuilderAcess.RunAndSave即可。
AssemblyBuilder完成创建之后,接下来要创建ModuleBuilder实例,它在后面被用来创建新的动态类型,然后使用AssemblyBuilder.DefineDynamicModule()方法创建一个新的动态模块,当然你可以在动态程序集里创建任意多的动态模块,但这里只需要创建一个。
非常幸运的是,一但AssemblyBuilder和ModuleBuilder创建成功,可以使用同样的实例来创建任意多的动态类型,所以他们只需创建一次。
接下来,为了创建真正的动态类型,你必须创建一个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类型的枚举,该枚举定义了类型的所有属性,第三个参数代表动态类型的父类型,本例中是System.Object,最后一个参数是动态类型实现的接口列表,这个参数非常重要,所以我将IORMapper传递给了该参数。
这里有一点需要指出,你是否有注意到我们通过Reflection.Emit中的类创建实例的一般模式呢?AppDomain用来创建AssemblyBuilder,AssemblyBuilder用来创建ModuleBuilder,ModuleBuilder用来创建TypeBuilder,这是工厂模式的另一个例子,在Reflection.Emit命名空间中经常用到。你或许已经猜到如何创建MethodBuilder,ConstructorBuilder,FieldBuilder,甚至是PropertyBuilder,没错,使用TypeBuilder!
我不想学IL中间码!
现在是时候深入使用Reflection.Emit中的类来写一些IL中间码了,如果你不希望花过多的时间去研究IL中间码规范或者相关其他文档,却又因为工作不得不使用它,没关系,微软为我们在Visual Studio .NET中提供了一个工具ILDasm.exe,它会给你带来巨大的帮助。
ILDasm可以让你查看程序集内部结构,特别是元数据和IL中间码,它们是程序集的重要组成部分,这就是我提到的在创建动态类型时,它会为我们提供巨大的帮助地方。与其去想如何通过IL中间码生成动态类型,不如直接使用C#代码写出原型代码,编译到程序集中,并通过ILDasm反编译出它的IL中间码,要弄清楚这些IL中间码的意图就轻而易举了,然后通过Reflection.Emit中的类将其重写即可。ILDasm对于我学习和理解创建动态类型的来龙去脉提供了巨大的帮助。
虽然在创建动态类型的时候不需要理解IL中间码,但是如果对IL中间码的语法或和操作码以及基于堆栈编程的原理有一些基本的理解,那么会对你非常有帮助。但是这里我不打算在涉及它,在文章的末尾我会推荐一些阅读资料,它们对于我IL中间码的学习起了很大的帮助。
Reflection.Emit“发出”方法剖析
无论是编写方法、构造器、或者是属性,本质上都是在编写方法,这个方法包含一个实现了某个功能的代码块;此外,在使用Reflection.Emit定义这些类型的时候,还有一些地方需要注意。
在C#中,如果类型的默认构造函数中没有任何逻辑,是不需要显式定义的,C#编译器会自动生成默认构造函数。同样的,IL和Reflection.Emit中,ilasm.exe和TypeBuilder.CreateType()方法会在幕后为你打点好一切。在本例中,构造方法中没有任何功能逻辑,但我还是来显式定义它,因为这是一个很好的查看“发出”方法的示例,而且也足够简单。
创建TypeBuilder实例之后,我们需要创建ConstructorBuilder实例,TypeBuilder.DefineConstructor()方法接受3个参数:MethodAttributes枚举,CallingConventions枚举和表示构造函数的参数列表的Type类型数组,代码如下:
ConstructorBuilder constructor = typeBuilder.DefineConstructor( MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName, CallingConventions.Standard, Type.EmptyTypes);
那么我如何知道在定义构造器时如何使用SpecialName和RTSpecialName的呢,我的确不知道,事实上我通过ILDasm.exe偷看了C#编译器是如何创建构造方法的,下面的代码是ILDasm.exe反编译之后的代码,它展示了C#编译器是如何定义一个默认构造方法的:
.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 }
请注意,代码中并未定义MethodAttributes.Instance的值,但是IL中间码中有一个“实例”属性分配给了构造函数的定义,这是因为MethodAttributes枚举没有定义该值,而是定义了MethodAttribtes.Static值,如果它的Static值未设置,那么ConstructorBuilder会默认设定为“实例”属性。
传入DefineConstructor()方法的第二个参数是CallingConventions.Standard,MSDN文档中对于该枚举的不同枚举值之间的区别的资料很少,但是我可以告诉你的是,若果传递Standard值,CLR会为你指定合适的CallingConventions值,所以我总是使用该值。
最后一个传入值是Type数组,所有的功能组建方法都有这个参数,它与方法定义的每个参数的类型相对应,数组中的每个参数类型必须与方法参数列表中的顺序一致。由于默认构造函数没有任何参数,所以我们使用Type.EmptyTypes,它是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 ConstructorInfo 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类,但是你不需要显示调用基类的构造方法(当然如果你想调用也可以没有任何问题)。但是在IL中间码中,必须显示调用它,在本例中,我们需要调用System.Object的默认构造函数。
为了使用Reflection.Emit显示调用默认构造函数,我们需要从创建ILGeneator实例开始,它是动态类型创建大部分工作的核心。ILGeneator的Emit()方法用于实际将IL操作码发出到新建方法中,它的实例是通过你目前在使用的创建者(ConstructorBuilder,MethodBuilder, PropertyBuilder等)对象来建立的,Emit()有17个重载方法,我不会每个方法都过一遍,但是所有的重载方法都会接受一个OpCodes类的静态属性值作为第一个参数值,而第二个参数则根据具体的IL操作码参数来决定是否需要传入。
另外须要牢记的一点是,对于IL或者Reflection.Emit,创建实例方法的时候,每一个实例方法都有一个隐藏参数,它是对该方法所在类对象的引用,始终作为方法的第一个传入参数。这就是我们可以在C#代码中使用的this关键字,或者VB.NET中的Me关键字的原因,这非常重要,因为在任何时候,当需要调用类实例方法或字段时,都必须使用该参数引用调用类型的实例。另一个有关IL有趣的珍闻是,所有的参数总是从参数列表的位置索引,所以我刚才提到的隐藏参数总是处于索引位置0,方法显式定义的参数总是从索引位置1开始。也就是说,每个实例方法至少有一个参数,包括默认构造函数和属性getter。但是对于静态方法而言,并没有这个隐藏的参数,方法显式定义的参数的引用索引位置从0开始。
IL中间码和Reflection.Emit还有一个很大的区别,注意上面的IL代码的第IL_001行,使用操作码“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中间码中,必须显示调用,最后用操作码OpCodes.Ret通知线程退出构造方法,该操作在每个方法的末尾都必须执行。
构造方法和其他一样,唯一的区别是它没有返回值。在IL中,当OpCodes.Ret被调用时,CLR会获取被调用方计算堆栈栈顶的返回值并尝试返回。如果在构造函数中,计算堆栈中有值存在,那就会出问题。如果获取返回值时候,计算堆栈为空,你会得到一个“Common Language Runtime detected an invalid program”错误,更糟糕的是当你在运行程序时要调用该构造方法实例化一个对象的时候,系统才会报错。(在运行之前验证IL中间码语法是否正确,参见本文“如何验证我的IL是否正确”部分)。事实上在调用Ret操作码时,上只要栈不为空,就会报错。你可以通过下面的代码来做一个测试:
il.Emit(OpCodes.Ldc_I4_3); //il.Emit(OpCodes.Pop);
运行测试程序,报错了,对吧?代码第一行表示将整数值3作为Int32类型推送到计算堆栈上。当调用操作码Ret时,CLR发现数值3存在于计算堆栈中,但方法的返回类型却是void,所以它抛出了异常。第二行注释掉的代码的作用是移除当前位于计算堆栈栈顶的值,取消注释,执行Ret操作时,计算堆栈为空,构造方法就可以正确返回了。
为动态类型创建带参数的构造函数、方法和属性都可以遵循这个基本结构。对于DataRow的列到对象属性映射的实现,我就不再赘述了,因为上文我已经阐述了所需的基本知识。记住最重要的一点,使用Reflection.Emit创建方法,最简单的途径是写出对应的C#代码,然后通过ILDasm.exe反编译出IL中间码,创建ILGenerator实例使用Emit方法将IL代码发出到动态类型,但是要保证动态类型的IL中间码和ILDasm反编译出IL中间码的结构完全一致。
一切就绪,但是在创建动态类型之前,我想再简单阐述一下工厂类如何创建动态类型实例并返回给调用者以及如何使用返回的动态类型:
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,如果还未创建,就调用之前定义的私有方法为动态类型创建DynamicAssembly,DynamicModule和TypeBuilder,构造函数和PopulateObject()方法。这一切都准备好之后,调用TypeBuilder.CreateType()创建一个Type类型的实例,通过该实例,调用CreateConstructor()方法,然后调用构造函数创建一个动态类型的工作实例。
到了关键的时刻了,创建一个新的类型并调用该类型的构造函数会调用之前通过IL建造的构造函数,任何错误的IL发出,都将导致CLR抛出异常。如果一切顺利,ObjectMapper_<TypeName>实例就会成功创建,然后工厂方法会将它强制转换为IORMapper类型并返回。
使用动态类型非常简单,只需记住面向接口编程,因为类设计时并不存在。下面的代码调用ObjectMapperFactory返回IORMapper实例。如果这是首次调用工厂类,它会产生ORMapper类并返回它的实例。之后工厂不再重新生成类,而是直接生成该类型的一个实例返回。然后用户调用PopulateObject()方法传入DataRow和Customer实例,代码如下:
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对象并填充数据。
现在已经完成了动态类型工厂的创建,在Visual Studio中编译通过。如果我运行它,它一定会工作吗,那可不一定。Reflection.Emit的不足之处在于,可以发出任意你想要的IL组合,但是却没有设计时编译器检查你写的IL是否有效。有时候通过TypeBuilder.CreateType()方法“烘烤”动态类型时,如果某些地方不对,它会报错,但这只针对某些已知的问题。有时候只有当你第一次调用方法的时候才会报错。还记得JIT编译器吗?它不会尝试编译和验证IL直到第一次真正调用这个方法。事实上,你很可能不会发现你的IL是无效的,直到你真正运行你的程序,或者你第一次调用生成的动态类型。CLR会提供有效的错误提示信息,对吧?恐怕不会!我获得过的最有帮助 的信息是“CLR运行时检查到无效的程序”异常。
那么如何验证IL中间码的正确性呢,PEVerify.exe!它是一个.NET工具,用于检查程序集IL中间码、结构和元数据的有效性。要使用PEVerify.exe,必须将动态程序集保存到物理文件中(记住,到现在为止,动态程序集只存在于内存中)。为了将动态程序集保存到文件中,需要对之前的代码做一些改动。首先,将AppDomain.DefineDynamicAssembly()方法传递的最后一个参数值修改为AssemblyBuilderAccess.RunAndSave;其次,修改AssemblyBuilder.DefineDynamicModule()方法的第二个参数传入程序集名称;最后在TypeBuilder.CreateType()方法后添加一行代码将保存程序集到文件中,代码如下:
Type draType = typeBuilder.CreateType(); assBuilder.Save(assBuilder.GetName().Name + ".dll");
一切就绪,运行应用程序并创建动态类型,在解决方案的Debug文件夹中会生成DynamicObjectMapper.dll文件(假设你使用的是Debug生成模式),现在打开.NET命令提示符窗口,输入PEVerify <程序集的路径>\DynamicObjectMapper.dll然后按下回车键,PEVerify将验证程序集,并且最后告诉你是否有错。PEVerify很棒的一点是它会提供非常详尽的错误信息,包括是什么错误,哪里出错了。另外需要注意的是,PEVerify验证失败并不意味着程序集一定无法运行。例如,当我第一次写工厂类时,使用“callvirt”操作码调用静态方法String.Equals(),这导致PEVerify验证失败,但是它依然可以运行。这是一个非常简单的错误,调用静态方法时应该使用“call”操作码而不是“callvirt“,修改后再次运行PEVerify就没有出任问题了。
最后一点,如果修改了代码将程序集保存到文件中,之后必须将其改为之前的代码。这是因为一旦将动态程序集保存到文件中,它将被锁定,将无法向其中添加新的动态类型。这是一个令人头疼的问题,因为应用程序有可能根据不同的类型创建多个mapper对象,如果在第一个类型生成之后将它们保存到文件中,将引发异常。
那么.NET 2.0中有什么新东西呢,有一个被称为轻量级代码生成(LCG)新功能非常不错,它提供了一种更快的方式创建全局静态方法,但是这里才是最酷的地方,你不需要创建动态程序集、动态模块和动态类型来“发出”方法,你可以直接发出到主程序的程序集中!只需通过其6个构造函数(不需要工厂方法)创建一个新的DynamicMethod对象,接下来创建你的ILGenerator并发出你的IL操作码,再调用这个方法,你也可以调用DynamicMethod.Invoke()方法或者使用Dynamic.CreateDelegate()方法获取引用该动态方法的委托实例,最后任何时候你都可以调用它了。
LCG和.NET 2.0中的另一项新特性匿名方法似乎很相似,匿名方法是属于全局静态方法,不属于任何类,并且作为委托暴露和调用。如果在幕后DynamicMethod类仅仅在程序集中创建了一个匿名方法就实现了LCG,我一点都不意外,因为DynamicMethod暴露出了返回委托类型的方法。
本文提出的解决方案一样可以很容易地通过DynamicMethod类实现。因为它只是有一个公共方法的类,是使用LCG的完美候选者。只需要对工厂中的方法进行一点点的修改。你可以在设计一个DataRowAdapter类取代工厂创建的并返回给用户的IDataRowAdapter实例,在该类中定义一个私有的委托类型变量和公有方法GetOrdinal(),该方法只调用委托的Invoke()方法然后获取返回值。工厂类只负责创建DataRowAdapter和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,推荐《Inside Microsoft .NET IL Assembler》,作者是Serge Lindin,这本书是毋庸置疑的经典之作,他也正在准备这本书的第二版,将会覆盖.NET 2.0的内容,而且定于今年5月发布。
本文为译文,作者为jconwell,原文地址:Introduction to Creating Dynamic Types with Reflection.Emit