反射发出动态类型(下)

引言

  在上一篇文章中,我介绍了动态类型以及它的用途,然后顺便提了一下关于如何使用动态类型来实现一个解决方案,但是都过于空洞,那么就让我们通过本文深入到实际的代码中去看看动态类型的实现和调用。

  首先简单回顾一下什么是动态类型,因为有些读者没有阅读过本文的第一部分或者希望跳过上篇文章直接阅读本文。

  所谓动态类型,就是运行时在程序内部动态生成的类或者类型。当应用程序启动后,至少会运行一个AppDomain,为了向AppDomain中添加动态类型,首先需要创建动态程序集,顾名思义,动态程序集就是在运行时创建并添加到AppDomain的程序集,它通常不会被保存到文件中,而是单独寄宿于内存中。动态程序集创建后,只需要很少的几步就可以使用Reflection.Emit创建动态类型了。

  那么动态类型都有哪些用途呢,第一,它们真的很酷(对于开发者而言,没有更好的理由了),我的意思是说通过在运行时发出IL中间码到内存中来创建自定义类型,那简直太棒了。但是严肃地讲,动态类型可以让你的应用程序评估数据的状态,而它们往往在运行之前都是未知的,从而创建一个针对当前情况进行优化的类。

  对于动态类型,最具挑战性的地方在于不能直接将你的C#代码直接插入到动态程序集,再通过C#编译器编译成IL中间码,否则那就太简单了。微软Reflection.Emit团队希望我们在工作中使用动态类型,使用Reflection.Emit中的类去定义和生成类型,方法,构造器和属性,然后插入或者“发出”IL操作码到这些定义中,听起来非常有趣吧?

问题陈述

  我从另一个开发者或者团队接手遗留的应用程序开发工作时,经常会遇到这样的问题,应用程序中采用DataSet从数据库获取数据,但是开发者总是使用整序数而不是串序数从DataRow中获取数据。

 1 //使用整序数:
 2 foreach (DataRow row in dataTable.Rows)
 3 {
 4     Customer c = new Customer();
 5     c.Address = row[0].ToString();
 6     c.City = row[1].ToString();
 7     c.CompanyName = row[2].ToString();
 8     c.ContactName = row[3].ToString();
 9     c.ContactTitle = row[4].ToString();
10     c.Country = row[5].ToString();
11     c.CustomerId = row[6].ToString();
12     customers.Add(c);
13 }
14 //使用串序数:
15 foreach (DataRow row in dataTable.Rows)
16 {
17     Customer c = new Customer();
18     c.Address = row["Address"].ToString();
19     c.City = row["City"].ToString();
20     c.CompanyName = row["CompanyName"].ToString();
21     c.ContactName = row["ContactName"].ToString();
22     c.ContactTitle = row["ContactTitle"].ToString();
23     c.Country = row["Country"].ToString();
24     c.CustomerId = row["CustomerID"].ToString();
25     customers.Add(c);
26 }
View Code

  任何重视性能的开发者都知道使用整序数比串序数更高效,我也完全认同这一点。为了证明他们的性能差异,我使用Nick Wienholt’s的性能测试框架(Performance Measurement Framework)(参考文章末尾的关于性能测试框架(Performance Measurement Framework))对这两种情况做了一个性能测试。相对于整序数测试,串序数的标准化测试持续时间(Normalized Test Duration(NTD))是4.87,也就是说使用整序数的效率几乎是使用串序数的3倍。在一个性能优先的应用程序的构建中,由于具有较高的用户负载,那一点点的时间差别很可能就令人无法接受,特别是当简单地使用整序数就可以得到很大的性能提升时。

  但是,使用整序数总是会带来麻烦的维护问题,这个问题一直困扰着我。如果DBA重新设计了表结构并在中间的某个位置增加了新的一列而不是在表的末尾,如果整张表的所有列都被打乱顺序重新构建,而且又假如开发者并未被通知有如上的改动,会发生什么呢?最有可能的结果是,应用程序将崩溃,因为它试图将SQL数据类型转化为与之不匹配的.NET数据类型。或者更糟的是,应用程序没有崩溃,继续获取已经被损坏的数据。

  不管你信不信,这种情况时常发生(至少我经常遇到)。最近由于Web服务访问量的增加,而这些服务同时由第三方供应商和另一个开发团队维护,所以应用程序很容易受到影响。这种情况促使我写一个工具类,它既可以获得整序数的高性能,又可以兼具串序数的可维护性。

解决方案

  为了解决这个问题,请看下面的类:

 1 public class DataRowAdapter
 2 {
 3     private static bool isInitialized = false;
 4     private static int[] rows = null;             
 5     public static void Initialize(DataSet ds)
 6     {
 7         if (isInitialized) return;
 8         rows = new int[ds.Tables[0].Columns.Count];
 9         rows[0] = ds.Tables[0].Columns["Address"].Ordinal;
10         rows[1] = ds.Tables[0].Columns["City"].Ordinal;
11         rows[2] = ds.Tables[0].Columns["CompanyName"].Ordinal;
12         rows[3] = ds.Tables[0].Columns["ContactName"].Ordinal;
13         .
14         .//使用列名获取余下的整序数
15         .
16         isInitialized = true;
17     }
18     //静态属性,用于返回整序数 
19     public static int Address { get {return rows[0];} }
20     public static int City { get {return rows[1];} }
21     public static int CompanyName { get {return rows[2];} }
22     public static int ContactName { get {return rows[3];} } 
23 }
View Code

  这个类很简单,在静态方法Initialize()中传入DataSet,然后遍历DataTable中的每一列并保存整序数到整型数组中。然后我定义了一组静态属性向列传回整序数。下面的代码展示了该类如何从DataRow中获取数据:

 1 DataRowAdapter.Initialize(dataSet);
 2 foreach (DataRow row in dataSet.Tables[0].Rows)
 3 {
 4     Customer c = new Customer();
 5     c.Address = row[DataRowAdapter.Address].ToString();
 6     c.City = row[DataRowAdapter.City].ToString();
 7     c.CompanyName = row[DataRowAdapter.CompanyName].ToString();
 8     c.ContactName = row[DataRowAdapter.ContactName].ToString();
 9     .
10     .
11     customers.Add(c);
12 }
View Code

  非常容易理解,DataRowAdapter扮演了一个整序数获取工具的角色,这样,代码就以伪串序数的方式从DataRow中获取数据,但是在幕后,实际上还是通过列的整序数来访问数据的。现在如果DBA改变了数据列的顺序,就不需要修改数据访问代码了。

  为了检验DataRowAdapter是否真的对性能提升有所帮助,我运行了一个性能测试,用以对比使用这种方式和直接使用整序数的性能差异,使用DataRowAdapter从DataRow访问数据的NTD为1.04,仅仅比直接使用整序数慢了4%,但是和直接使用串序数访问相比性能却提升了300%!

更好的解决方案

  这种方式在很长一段时间都工作的不错,直到使用DataTable的情况越来越多,我才意识到要维护这些类简直太痛苦了,我必须创建新类并且为每个DataTable列签名硬编码静态属性,在编写了15个不同的类之后,我简直要崩溃了。

  进入Reflection.Emit命名空间,其中有一大堆的类主要用于在运行时动态创建程序集和类型。这些东西之所以重要,是因为使用Reflection.Emit,你可以根据DataTable在运行时动态生成对应的DataRowAdapter类,这样就避免了直接硬编码一堆特定的静态类。理论上,你只需要向工厂类传入DataSet或者DataTable,工厂类应该根据DataTable的列结构生成新的DataRowAdapter类,而且一旦DataRowAdapter生成,就不需要再生成了,因为它已经被加载到AppDomain中了,很方便吧?

  使用Reflection.Emit创建动态类型的缺点(任何东西都有缺点)是,不能直接使用你的字符串C#代码来编译(事实上通过System.CodeDom命名空间和CSharpCodeProvider类可以做到这点,但是这样我们就必须通过C#编译器编译,然后再通过JIT编译器编译,那样就太慢了),通过Reflection.Emit,你可以在内存中创建程序集并直接发出IL操作码,这样做的优点是不需要通过C#编译器编译,因为你发出的代码就是IL中间码,缺点是你必须理解IL中间码,但是有很多方法都可以让它变得简单,我在后文就会提到。

定义接口

  在使用Reflection.Emit的时候,这里存在另外一个问题,没有相应的接口暴露出来,这些在运行时生成的类在设计时并不存在,那么你要怎么样来调用它们呢?哈哈,这就是接口的魔力!

  所以第一步应该确定好动态类型的公共接口,这里是一个十分简单的例子,所以接口也非常简单。所以经过再三思考后,接口定义如下:

1 public interface IDataRowAdapter
2 {
3     int GetOrdinal(string colName);
4 }
View Code

  因为我们不知道所需的列名,而接口也不能包含硬编码静态属性,因此取而代之的是,我定义了GetOrdinal()的方法,传入字符串类型的列名参数,返回对应列的整序数。

  工厂类生成的所有动态类型都会继承这个接口,并且这个接口也作为工厂类的返回类型。程序调用工厂类传入DataTable参数,然后获取一个实现了IDataRowAdapter的对象,就可以调用IDataRowAdapter.GetOrdinal()方法获取某一列对应的整序数了。

  除了定义一个统一接口让所有的动态类型实现它之外,还有另一种方案,你可以使用后期绑定通过反射访问动态类型的方法和属性。但是,这种方案非常糟糕,原因有以下几点;第一,接口是一个类型的契约,它保证了其中的方法存在并且可以调用。如果通过反射使用后期绑定方法调用,那么将无法保证特定类型的方法是否存在,你可能会拼错方法名,但是编译器不会报错,直到应用程序运行并且尝试调用这个方法时,才会被抛出反射异常。第二,使用反射会导致性能问题,任何因使用动态类型而获得的性能优势因此将不复存在。

方案选择

  在编写实现接口IDataRowAdapter的DataRowAdapter类的C#原型代码时,我尝试了几种不同的方法以决定如何将字符串列名转化为整序数值。因为我希望尽力找到一种更快的方法从DataRow中动态获取数据,我为每个方法都做了性能评估并且比较了评估结果。以下是几种不同的方法以及相比直接使用整序数的比较结果:

  1. 使用switch语句,将字符串列名传入;
  2. 为列名创建枚举,对枚举(预想应该比使用基于字符串的switch要快)使用switch语句。使用Enum.Parse(columnName)来创建枚举实例;
  3. 使用多个if语句来检查列名;
  4. 使用Dictionary<string, int>来存储列名/序数映射;

  结果有些令人意外。最糟糕的是使用基于枚举的switch语句。这是因为Enum.Parse()使用了反射去创建列枚举的实例,这种方式相比整序数的NTD是10.74。

  其次是基于字符串的switch语句,相比整序数的NTD是3.71,比直接使用串序数快不了多少。

  接下来才是使用泛型字典,相比整序数的NTD是3.4,效率依然不高。

  性能最高的是使用多个if语句,相比整序数的NTD是2.6,和直接使用整序数查询相比仍然差的很远,但是相比串序数已经很快了,并且还能获得直接使用列名的所带来的安全性。

  如下代码是我决定的最后实现,这就是我要使用Reflection.Emit生成的类型的IL中间码的原型:

 1 public class DataRowAdapter : IDataRowAdapter
 2 {
 3     public int GetOrdinal(string colName)
 4     {
 5         if (colName == "Address")
 6                     return 0;
 7         if (colName == "City")
 8                     return 1;
 9         if (colName == "CompanyName")
10                     return 2;
11         if (colName == "ContactName")
12                     return 3;
13         .
14         .
15         throw new ApplicationException("Column not found");
16     }
17 }
View Code

  现在看到动态类型的好处了吧,你完全不需要像如上代码那样在设计时硬编码,因为你不知道City列的序数是位置1还是其他位置。但是通过Reflection.Emit,你一定知道,因为该类就是基于运行时的数据生成的。

解决方案设计

  下面我们要设计一下生成动态类型并返回给调用者的类。对于动态类型生成器,我还是决定使用工厂模式。它非常适合这种情况,因为调用者无法显式调用动态类型的构造器。另外我也希望对调用者隐藏动态类型实现的细节,所以接口和工厂类的设计如下:

 1 public class DataRowAdapterFactory
 2 {
 3     public static IDataRowAdapter CreateDataRowAdapter(DataSet ds, string tableName)
 4     {
 5         //方法实现
 6     }
 7     public static IDataRowAdapter CreateDataRowAdapter(DataTable dt)
 8     {
 9         //方法实现
10     }
11     //私有工厂方法
12 }
View Code

  因为每个动态类型都会对列名列表进行硬编码,每个传递给工厂的带有不同表名的DataTable都会导致工厂生成一个新的类型,如果DataTable第二次被传入工厂,DataTable对应的动态类型已经被生成了,所以工厂类只需返回一个已生成生成类型的实例即可。

建立动态类型

  (备注:Reflection.Emit中的一些功能描述可能与我上一篇文章有重复,但是我希望没有阅读过本文的第一部分的读者也可以直接阅读本文)。

  在编写GetOrdinal()方法之前,让我们来看一下如何建立程序集来承载新的类型,因为Reflection.Emit无法向已有的程序集添加新的类型,你必须通过AssemblyBuilder类在内存中生成一个全新的程序集:

 1 private static AssemblyBuilder asmBuilder = null;
 2 private static ModuleBuilder modBuilder = null;
 3 private static void GenerateAssemblyAndModule()
 4 {
 5     if (asmBuilder == null)
 6     {
 7         AssemblyName assemblyName = new AssemblyName();
 8         assemblyName.Name = "DynamicDataRowAdapter";
 9         AppDomain thisDomain = Thread.GetDomain();
10         asmBuilder = thisDomain.DefineDynamicAssembly(
11                      assemblyName, AssemblyBuilderAccess.Run);
12         modBuilder = assBuilder.DefineDynamicModule(
13                      asmBuilder.GetName().Name, false);
14     }
15 }
View Code

  要创建AssemblyBuilder实例,首先要创建AssemblyName实例并制定程序集的名称,然后调用Thread.GetDomain()方法获取AppDomain实例,该实例允许用DefineDynamicAssembly方法以指定名称和访问模式定义动态程序集,只需要向其传入AssemblyName的实例和AssemblyBuilderAccess的枚举值即可,这里我不想把程序集保存到文件中,如果想保存的话,只需使用枚举值AssemblyBuilderAcess.Save或者AssemblyBuilderAcess.RunAndSave即可。

  非常幸运的是,一但AssemblyBuilderModuleBuilder创建成功,可以使用同样的实例来创建任意多的动态类型,所以他们只需创建一次。

  AssemblyBuilder完成创建之后,接下来要创建ModuleBuilder实例,它在后面被用来创建新的动态类型,然后使用AssemblyBuilder.DefineDynamicModule()方法创建一个新的动态模块,当然你可以在动态程序集里创建任意多的动态模块,但这里只需创建一个。

  现在我们开始创建动态类型:

 1 private static TypeBuilder CreateType(ModuleBuilder modBuilder, string typeName)
 2 {
 3     TypeBuilder typeBuilder = modBuilder.DefineType(typeName, 
 4                 TypeAttributes.Public | 
 5                 TypeAttributes.Class |
 6                 TypeAttributes.AutoClass | 
 7                 TypeAttributes.AnsiClass | 
 8                 TypeAttributes.BeforeFieldInit | 
 9                 TypeAttributes.AutoLayout, 
10                 typeof(object), 
11                 new Type[] {typeof(IDataRowAdapter)});
12     return typeBuilder;
13 }
View Code

  通过TypeBuilder就可以创建动态类型了。调用ModuleBuilder.DefineType()方法,创建TypeBuilder实例,第一个参数接受字符串类型的类型名称,第二个参数接受TypeAttributes类型的枚举,该枚举定义了类型的所有属性,第三个参数代表动态类型的父类型,本例中是System.Object,最后一个参数是动态类型实现的接口列表,这个参数非常重要,所以我将IDataRowAdapter传递给了该参数。

  这里有一点需要指出,你是否有注意到我们通过Reflection.Emit中的类创建实例的一般模式呢?AppDomain用来创建AssemblyBuilderAssemblyBuilder用来创建ModuleBuilderModuleBuilder用来创建TypeBuilder,这是工厂模式的另一个例子,在Reflection.Emit命名空间中经常用到。你或许已经猜到如何创建MethodBuilderConstructorBuilderFieldBuilder,甚至是PropertyBuilder,没错,使用TypeBuilder

动态类型带来的设计变更

  我想在这里谈一谈我的设计,关于最终的DataRowAdapter,通过多个if语句来实现用字符串列名来决定应该返回哪一个序数的原型,因为类型在运行时被创建,这里有一个更好的方法。比较两个整数明显要快于比较两个字符串是否相等,但是如何将字符串转化整数值呢?不妨试试string.GetHashCode(),在你吐槽哈希值无法保证每个字符串的唯一性之前,先听我解释,每个字符串的确不可能都获得唯一的哈希值,但是在一个很小的字符串列表范围内就非常可能了,比如一个DataTable的列名列表中。

  所以,我建立了一个方法用于检查在DataTable中的所有哈希值是否唯一。如果发现所有的列名都是唯一的,那么动态类型工厂将会采用switch语句检查整数值,如果发现他们中存在重复的项,那么动态类型工厂则采用多个if语句检查字符串的相等性。

  我想看看相比字符串的相等性比较,使用列名的哈希值比较它们之间的性能有多大的差异,以证明在类型工厂中引入的复杂度是否得不偿失。跑完性能测试,我发现和直接使用整序数相比,使用哈希值的NTD是1.35,而原始静态的DataRowAdapter的NTD是1.04,但是我却需要维护DataTable对应的每个类,而且如果一个应用程序很庞大的话,维护工作会更加繁重。而如果我们使用动态类型,那么所有的维护工作都省掉了。而且大部分时候,维护带来的好处比性能上带来的好处要重要的多,特别是在性能下降的不太厉害的情况下。

  为了获得使用字符串比较相等性时DataRowAdapter的运行速度,我做了一个测试,结果并不理想,它的NTD为1.9,是直接使用整序数的2倍,但是仍然要比直接使用串序数要快很多。但是我还是选择使用这种设计,因为列名列表中的字符串的哈希值有可能不是唯一的,虽然这种可能性很小。

  所以,如果使用这种设计,在大部分时间里你所获得的性能来自整数相等性检查,有时候则是字符串相等性检查,不管使用哪种方式,都比直接使用串序数要快。

使用Reflection.Emit实现GetOrdinal方法

  动态类型工厂的核心是创建GetOrdinal()方法,到现在为止,我还没有太多的涉及IL中间码的发出,现在让我们深入了解Reflection.Emit并使用它来发出IL中间码。

  下面是GetOrdinal方法的代码:

 1 private static void CreateGetOrdinal(TypeBuilder typeBuilder, DataTable dt)
 2 {
 3     int colIndex = 0;
 4     //create the needed type arrays
 5     Type[] oneStringArg =  new Type[1] {typeof(string)};
 6     Type[] twoStringArg = new Type[2] {typeof(string), typeof(string)};
 7     Type[] threeStringArg =  
 8        new Type[3] {typeof(string), typeof(string), typeof(string)};
 9     //create needed method and contructor info objects
10     ConstructorInfo appExceptionCtor = 
11        typeof(ApplicationException).GetConstructor(oneStringArg);
12     MethodInfo getHashCode  = typeof(string).GetMethod("GetHashCode");
13     MethodInfo stringConcat = typeof(string).GetMethod("Concat", threeStringArg);        
14     MethodInfo stringEquals = typeof(string).GetMethod("op_Equality", twoStringArg);
15     //defind the method builder
16     MethodBuilder method = typeBuilder.DefineMethod("GetOrdinal", 
17        MethodAttributes.Public | MethodAttributes.HideBySig | 
18        MethodAttributes.NewSlot | MethodAttributes.Virtual | 
19        MethodAttributes.Final, typeof(Int32), oneStringArg);              
20     //create IL Generator
21     ILGenerator il = method.GetILGenerator();
22     //define return jump label
23     System.Reflection.Emit.Label outLabel = il.DefineLabel();
24     //define return jump table used for the many if statements
25     System.Reflection.Emit.Label[] jumpTable = 
26                 new System.Reflection.Emit.Label[dt.Columns.Count];
27     if (AllUniqueHashValues(dt))
28     {
29         //create the return int index value, and hash value
30         LocalBuilder colRetIndex = il.DeclareLocal(typeof(Int32));
31         LocalBuilder parmHashValue = il.DeclareLocal(typeof(Int32));
32         il.Emit(OpCodes.Ldarg_1);
33         il.Emit(OpCodes.Callvirt, getHashCode);
34         il.Emit(OpCodes.Stloc_1);
35         foreach (DataColumn col in dt.Columns)
36         {
37             //define label
38             jumpTable[colIndex] = il.DefineLabel();
39             //compare the two hash codes
40             il.Emit(OpCodes.Ldloc_1);
41             il.Emit(OpCodes.Ldc_I4, col.ColumnName.GetHashCode());
42             il.Emit(OpCodes.Bne_Un, jumpTable[colIndex]);
43             //if equal, load the ordianal into loc0 and return
44             il.Emit(OpCodes.Ldc_I4, col.Ordinal);
45             il.Emit(OpCodes.Stloc_0);
46             il.Emit(OpCodes.Br, outLabel);
47             il.MarkLabel(jumpTable[colIndex]);
48 
49             colIndex++;
50         }
51     }
52     else
53     {
54         //create the return int index value, and hash value
55         LocalBuilder colRetIndex = il.DeclareLocal(typeof(Int32));     
56         foreach (DataColumn col in dt.Columns)
57         {
58             //define label
59             jumpTable[colIndex] = il.DefineLabel();
60             //compare the two strings
61             il.Emit(OpCodes.Ldarg_1);
62             il.Emit(OpCodes.Ldstr, col.ColumnName);
63             il.Emit(OpCodes.Call, stringEquals);
64             il.Emit(OpCodes.Brfalse, jumpTable[colIndex]);
65             //if equal, load the ordianal into loc0 and return
66             il.Emit(OpCodes.Ldc_I4, col.Ordinal);
67             il.Emit(OpCodes.Stloc_0);
68             il.Emit(OpCodes.Br, outLabel);
69             il.MarkLabel(jumpTable[colIndex]);
70 
71             colIndex++;
72         }
73     }
74           
75     //error handler if cant find column name
76     il.Emit(OpCodes.Ldstr, "Column '");
77     il.Emit(OpCodes.Ldarg_1);
78     il.Emit(OpCodes.Ldstr, "' not found");
79     il.Emit(OpCodes.Callvirt, stringConcat);
80     il.Emit(OpCodes.Newobj, appExceptionCtor);
81     il.Emit(OpCodes.Throw);
82     //label for if user found column name
83     il.MarkLabel(outLabel);
84     //return ordinal for column
85     il.Emit(OpCodes.Ldloc_0);
86     il.Emit(OpCodes.Ret);
87 }
View Code

  我们要做的第一件事就是建立稍后需要使用的东西,在将要发出的方法GetOrdinal()的生命周期中,需要4个方法,它们是string.GetHashCode(),string.Concat(),string.op_Equality()和ApplicationException类的构造方法。我们还需要创建MethodInfo和ConstructorInfo对象用于这些方法发出“call”或者“callvirt”操作。

  下一步使用TypeBuilder创建MethodBuilder实例,再次申明,在定义MethodBuilder时,我完全是依照C#原型代码编译后再通过ILDASM来查看MethodAttribute是如何使用的。TypeBuilder.DefineMethod()方法和DefineConstructor(参见本文的上半部分)方法类似,DefineMethod()方法只是多了一个定义返回类型的参数,如果你定义的方法没有返回值,那么就向该参数传入null。

  然后创建ILGenerator实例,这几乎和MethodBuilder创建的方法一样。

  为了使用标签(label),我们首先要使用ILGenerator.DefineLabel()方法进行定义,然后将其传入ILGenerator.Emit()方法的第二个参数,最后将这些分支操作码(Brfalse,Brtrue,Be,Bge,Ble,Br等等)传入Emit方法第一个参数,基本的意思是“if(条件为真,假,相等,大于等于,小于等于) 则跳转到该标签”,最后使用ILGenerator.MarkLabel()方法并传入Label实例进行标记,目的是通知应用程序当满足分支操作的条件时就跳转到被标记的Label。对于条件语句,需要对其在分支操作码定义之后进行标记,而对于循环语句,通常要在分支操作码定义之前进行标记。

  那么如何知道发出什么样的IL中间码到GetOrdinal()方法中呢?和定义MethodBuilder类的方法一样,先编写出C#代码并编译,使用ILDasm.exe查看生成的IL中间码。只要从ILDASM获取了IL中间码,剩下的工作就很简单了,只需要重复繁冗的IL发出语句ILGenerator.Emit(Opcodes.*)就可以了。

  这里我就解释GetOrdinal方法的每一行代码了,因为对照C#版本的GetOrdinal()一看就都明白了。

创建和使用动态类型实例

  现在所有用于创建动态类型的工具都已准备就绪,申明最后一点:工厂类如何创建动态类型和返回实例给调用者,以及如何使用动态类型。如下代码展示了工厂类的基本结构:

 1 public static IDataRowAdapter CreateDataRowAdapter(DataTable dt)
 2 {
 3     return CreateDataRowAdapter(dt, true);
 4 }
 5 
 6 TypeBuilder typeBuilder = null;
 7 
 8 private static IDataRowAdapter CreateDataRowAdapter(DataTable dt, 
 9                                bool returnAdapter)
10 {
11     //return no adapter if no columns or no table name
12     if (dt.Columns.Count == 0 || dt.TableName.Length == 0)
13                 return null;
14     //check to see if type instance is already created
15     if (adapters.ContainsKey(dt.TableName))
16                 return (IDataRowAdapter)adapters[dt.TableName];
17     //Create assembly and module 
18     GenerateAssemblyAndModule();
19     //create new type for table name
20     TypeBuilder typeBuilder = CreateType(modBuilder, "DataRowAdapter_" + 
21                               dt.TableName.Replace(" ", ""));
22     //create get ordinal
23     CreateGetOrdinal(typeBuilder, dt);
24     IDataRowAdapter dra = null;
25           
26     Type draType = typeBuilder.CreateType();
27     //assBuilder.Save(assBuilder.GetName().Name + ".dll");
28     //Create an instance of the DataRowAdapter
29     IDataRowAdapter dra = (IDataRowAdapter) = 
30                     Activator.CreateInstance(draType, true);
31     //cache adapter instance
32     adapters.Add(dt.TableName, dra);
33     //if just initializing adapter, dont return instance
34     if (!returnAdapter)
35                 return null;
36     return dra;
37 }
View Code

  工厂类首先检查TypeBuilder是否已被创建。如果该类没有创建,工厂类才调用之前我展示给大家的私有方法创建DynamicAssembly,DynamicModule,TypeBuilder和GetOrdinal方法。所有的这些步骤完成之后,使用TypeBuilder.CreateType()方法就可以为DataRowAdapter返回Type实例,再调用Activator.CreateInstance方法并使用生成的类型就可以创建动态类型的工作实例了。

  是见证真理的时刻了,在该类型上创建新的类型和调用构造器将会调用之前我们建立的构造器。如果IL中间码的发出有任何错误,CLR都会抛出异常。如果一切顺利,会生成一个DataRowAdapter的工作实例。由于工厂类已经拥有了一个DataRowAdapter实例,它将会被直接转化为IDataRowAdapter类型并返回。

  使用动态类型非常简单。只需记住一点:面向接口编程。如下代码展示了如何调用DataRowAdapter工厂类并返回DataRowAdapter实例,工厂类返回IDataRowAdapter实例,然后并传入对应列名调用GetOrdinal方法,DataRowAdapter会自动查询与之匹配的整序数并返回。代码如下:

 1 IDataRowAdapter dra = DataRowAdapterFactory.CreateDataRowAdapter(dataTable);
 2 foreach (DataRow row in data.Rows)
 3 {
 4     Customer c = new Customer();
 5     c.Address = row[dra.GetOrdinal("Address")].ToString();
 6     c.City = row[dra.GetOrdinal("City")].ToString();
 7     c.CompanyName = row[dra.GetOrdinal("CompanyName")].ToString();
 8     c.ContactName = row[dra.GetOrdinal("ContactName")].ToString();
 9     .
10     .//pull the rest of the values
11     customers.Add(c);
12 }
View Code

  如下的类图和序列图说明了如何使用工厂类创建一个IDataRowAdapter类型的实例和它是如何被消费者使用的:

如何验证我的IL

  (这部分和本系列文章的第一部分有重复,在这里仅仅作为阅读引导)

  现在已经完成了动态类型工厂的创建,在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()方法后添加一行代码将保存程序集到文件中,代码如下:

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

  一切就绪,运行应用程序并创建动态类型,在解决方案的Debug文件夹中会生成DynamicDataRowAdapter.dll文件(假设你使用的是Debug生成模式),现在打开.NET命令提示符窗口,输入PEVerify  <程序集的路径>\ DynamicDataRowAdapter.dll然后按下回车键,PEVerify将验证程序集,并且最后告诉你是否有错。PEVerify很棒的一点是它会提供非常详尽的错误信息,包括是什么错误,哪里出错了。另外需要注意的是,PEVerify验证失败并不意味着程序集一定无法运行。例如,当我第一次写工厂类时,使用“callvirt”操作码调用静态方法String.Equals(),这导致PEVerify验证失败,但是它依然可以运行。这是一个非常简单的错误,调用静态方法时应该使用“call”操作码而不是“callvirt“,修改后再次运行PEVerify就没有出任问题了。

  最后一点,如果修改了代码将程序集保存到文件中,之后必须将其改为之前的代码。这是因为一旦将动态程序集保存到文件中,它将被锁定,将无法向其中添加新的动态类型。这是一个令人头疼的问题,因为每次向工厂传入新的DataTable,都会创建一个新的动态类型,如果在第一个类型生成之后,将新的类型再次保存到文件中时将引发异常。

  另一种检查IL中间码的方法是使用Reflector将IL中间码反编译为C#代码再查看你所发出的代码是否正确。

关于过度设计

  以上做了这么多极度复杂的工作似乎只为了那么一点点的性能提升,这样做真的值得吗?也许不值得,也许值得,这取决于你所开发的应用程序。一些服务端应用程序负载沉重,哪怕是一点点的性能提升都显得相当重要。

  本文的目的不是要给大家介绍一个新的工具类,而是让大家感受一下使用Reflection.Emit能做些什么,比如有很大的可能该系列的下篇文章我就会讲述一个完全使用Reflection.Emit和动态代理实现的简单的AOP框架。

调试动态类型

  动态类型也有黑暗的一面,极难维护和调试。而且在开发中还存在一个问题,我称它为“车祸(Hit by a bus)”问题。IL中间码极其复杂,懂IL的人不多。所以,当你的开发团队中只有一人懂IL,但是这个人却出了车祸的时候会怎样呢?谁来维护呢?我不是说应该放弃动态类型带来的那些已被证明的优点,至少有2个人理解产品中的复杂部分总是好的。

  第二个问题是调试,因为IL中间码直接被写入内存,如果你调试过动态类型你就会知道,很难在Visual Studio中创建一个断点并单步调试进入你的IL代码的。当然也有替代方法和解决方案,我以后会在有关动态类型的文章中单独讲述。

整序数的性能

  在本文中,我对使用串序数和整序数访问DataRow的性能进行了评估,虽然使用整序数的性能是串序数的4倍,却并不意味着你的页面加载速度就会比原来快4倍,甚至还有可能差的很远。从DataReader中获取数据只是页面要做的很小的一部分工作。尤其是当页面使用了任何类型的数据绑定,数据绑定的开销是非常昂贵的,它的性能开销最有可能将通过整序数带来的性能优势抵消得所剩无几。所以不要期望在页面加载中看到明显的性能提升。如果在单位时间内有大量用户并发请求网页的情况下,使用压力测试工具或许能才能看到那么一点点。

性能测试框架

  本文中提到的所有性能测试都采用了Nick Wienholt的性能测试框架(Performance Measurement Framework)。你可以在这里www.dotnetperformance.com(遗憾的是该网站目前无法访问)下载源代码和查看怎样使用它的文章。所有的测试结果都是将被测代码块执行5000次,在此持续时间内的时间测量结果,然后重复10次,得到10次测量结果,再将这10次测量结果放在一起,计算出统计数据,比如标准化值,中值,平均值,最小值和最大值以及标准差。性能测试框架为你做了所有的这一切,而你只需要编写被测代码块并设置好框架插件,然后运行就可以了。

Reflection.Emit的替代方案

  除了Reflection.Emit,还可以使用System.CodeDom对象图来表示生成的代码,然后通过CodeDomProvider在内存中创建程序集并将其加载到AppDomain中。作为一种选择,你也可以使用CodeDom类将字符串形式的C#代码插入到CodeSnippetExpression中,然后通过CodeDomProvider来运行它。使用这两种方法都可以,但是使用动态类型则会带来更高效的性能。以上两种方法都必须经C#编译器编译,并将生成的程序集手动加载到AppDomain中,然后才可以调用。而使用Reflection.Emit则可以省去这些额外的步骤。

本文为译文,作者为jconwell,原文地址:Introduction to Creating Dynamic Types with Reflection.Emit: Part 2 附件代码:下载

posted @ 2013-08-16 10:05 LukyW 阅读(...) 评论(...) 编辑 收藏