文章出處

引言

  在上一篇文章中,我介紹了動態類型以及它的用途,然后順便提了一下關于如何使用動態類型來實現一個解決方案,但是都過于空洞,那么就讓我們通過本文深入到實際的代碼中去看看動態類型的實現和調用。

  首先簡單回顧一下什么是動態類型,因為有些讀者沒有閱讀過本文的第一部分或者希望跳過上篇文章直接閱讀本文。

  所謂動態類型,就是運行時在程序內部動態生成的類或者類型。當應用程序啟動后,至少會運行一個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 附件代碼:下載


文章列表


不含病毒。www.avast.com
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()