問題回顧
在上篇博客中,我介紹了優化反射的第一個步驟:用委托調用代替直接反射調用。
然而,那只是反射優化過程的開始,因為新的問題出現了:如何保存大量的委托?
如果我們將委托保存在字典集合中,會發現這種設計會浪費較多的執行時間,因為這種設計會引發三個新問題:
1. 代碼的執行路徑變長了。
2. 字典查找是有成本開銷的。
3. 字典集合的並發讀寫需要鎖定,會影響並發性。
再來回顧一下上次的測試結果吧:
雖然通用接口ISetValue將反射性能優化了37倍,但是最終的FastSetValue將這個數字減少到還不到7倍(在CLR4中還不到5倍)。
難道您不覺得遺憾嗎?
再看看直接調用與反射調用的對比,它們的速度相差了上千倍!
能不能不使用委托?
既然委托最后引出了三個難以解決的問題,導致優化后速度比直接調用差距太遠,那我們能不能不使用委托呢?
委托調用並不是優化反射的唯一方案,我們還有其它方法,
之所以委托調用能成為常見的優化方案是因為它比較簡單。
假如我需要用客戶端提交的數據來填充某個數據對象,考慮到代碼的通用性,我會用反射寫成這樣:
/// <summary> /// 從HttpRequest加載obj所需的數據 /// </summary> /// <param name="request"></param> /// <param name="obj"></param> public static void LoadDataFromHttpRequest(HttpRequest request, object obj) { PropertyInfo[] properties = obj.GetType().GetProperties(); foreach( PropertyInfo p in properties ) { // 這里只是示意代碼,假設數據處理不會有異常。 object val = Convert.ChangeType(request[p.Name], p.PropertyType); p.FastSetValue(obj, val); } }
如果我事先知道要加載已知的數據類型,代碼會寫成這樣:
public static void LoadDataFromHttpRequest(HttpRequest request, OrderInfo order) { // 這里只是示意代碼,假設數據處理不會有異常。 order.OrderID = int.Parse(request["OrderID"]); order.OrderDate = DateTime.Parse(request["OrderDate"]); order.SumMoney = decimal.Parse(request["SumMoney"]); order.Comment = request["Comment"]; order.Finished = bool.Parse(request["Finished"]); }
顯然,第二段代碼運行效率更快(盡管第一段代碼調用FastSetValue優化了速度)。
大家都知道反射性能較差,直接調用性能最好,那么能不能在運行時不使用反射呢?
的確,使用反射是因為我們事先不知道要處理哪些類型的對象,因此不得不用反射, 另外,反射的代碼也更通用,寫一個方法可以加載所有的數據類型,可認為是一勞永逸的方法。 不過,就算我們事先不知道要處理哪些對象類型,但是只要使用反射,我們完全可以知道任何一個類型包含哪些數據成員, 還能知道這些數據成員的數據類型,這一點不用懷疑吧? 既然我們用反射可以知道所有的類型定義信息,我們是否可以參照代碼生成器的思路去生成代碼呢? 我們可以參照前面第二段代碼,為【需要處理的類型】生成直接調用的代碼,這樣不就徹底解決了反射性能問題了嗎? 生成代碼的過程,其實也就是個字符串的拼接過程,難度並不大,只是比較復雜而已。
如果前面的答案都是肯定的,那么現在只有一個問題了:我們能在運行時執行拼接生成的字符串代碼嗎?
答案也是肯定的:能!
CodeDOM:在運行時編譯代碼
回憶一下我們編寫的ASPX頁面,它們並不是C#代碼,它們本質上就是一個文本文件, 我們可以寫入一些HTML標簽,還有些標簽上加了 runat="server" 屬性, 我們還可以在頁面中插入一些C#代碼片段,盡管它們不是我們編譯后的DLL文件,然而它們就是運行起來了! 要知道ASP.NET不是ASP,ASP是解釋性的腳本語言,而ASP.NET是以編譯方式運行的, 所以,每個ASPX頁面文件最后都是運行編譯后的結果。
假設我有下面一段文本(文本的內容是一段C#代碼):
using System; using System.Collections.Generic; using System.Text; using System.Reflection; namespace OptimizeReflection { public class DemoClass { public int Id { get; set; } public string Name; public int Add(int a, int b) { return a + b; } } public class 用戶手冊 { public static void Main() { // OptimizeReflection 這個類庫提供了一些擴展方法,它們用於優化常見的反射場景 // 下面是一些相關的演示示例。 // 對於屬性的讀寫操作、方法的調用操作,還提供了性能更好的強類型(泛型)版本,可參考Program.cs Type instanceType = typeof(DemoClass); PropertyInfo propertyInfo = instanceType.GetProperty("Id"); FieldInfo fieldInfo = instanceType.GetField("Name"); MethodInfo methodInfo = instanceType.GetMethod("Add"); // 1. 創建實例對象 DemoClass obj = (DemoClass)instanceType.FastNew(); // 2. 寫屬性 propertyInfo.FastSetValue(obj, 123); propertyInfo.FastSetValue2(obj, 123); // 3. 讀屬性 int a = (int)propertyInfo.FastGetValue(obj); int b = (int)propertyInfo.FastGetValue2(obj); // 4. 寫字段 fieldInfo.FastSetField(obj, "Fish Li"); // 5. 讀字段 string s = (string)fieldInfo.FastGetValue(obj); // 6. 調用方法 int c = (int)methodInfo.FastInvoke(obj, 1, 2); int d = (int)methodInfo.FastInvoke2(obj, 3, 4); Console.WriteLine("a={0}; b={1}; c={2}; d={3}; s={4}", a, b, c, d, s); } } }
您可以把上面這段文本想像成前面第二個版本的LoadDataFromHttpRequest方法,如果我們在運行時使用反射也能生成那樣的代碼, 現在就差把它編譯成程序集了。下面的代碼演示了如何將一段文本編譯成程序集的過程:
string code = null; // 1. 生成要編譯的代碼。(示例為了簡單直接從程序集內的資源中讀取) Stream stram = typeof(CodeDOM).Assembly .GetManifestResourceStream("TestOptimizeReflection.用戶手冊.txt"); using( StreamReader sr = new StreamReader(stram) ) { code = sr.ReadToEnd(); } //Console.WriteLine(code); // 2. 設置編譯參數,主要是指定將要引用哪些程序集 CompilerParameters cp = new CompilerParameters(); cp.GenerateExecutable = false; cp.GenerateInMemory = true; cp.ReferencedAssemblies.Add("System.dll"); cp.ReferencedAssemblies.Add("OptimizeReflection.dll"); // 3. 獲取編譯器並編譯代碼 // 由於我的代碼使用了【自動屬性】特性,所以需要 C# .3.5版本的編譯器。 // 獲取與CLR匹配版本的C#編譯器可以這樣寫:CodeDomProvider.CreateProvider("CSharp") Dictionary<string, string> dict = new Dictionary<string, string>(); dict["CompilerVersion"] = "v3.5"; dict["WarnAsError"] = "false"; CSharpCodeProvider csProvider = new CSharpCodeProvider(dict); CompilerResults cr = csProvider.CompileAssemblyFromSource(cp, code); // 4. 檢查有沒有編譯錯誤 if( cr.Errors != null && cr.Errors.HasErrors ) { foreach( CompilerError error in cr.Errors ) Console.WriteLine(error.ErrorText); return; } // 5. 獲取編譯結果,它是編譯后的程序集 Assembly asm = cr.CompiledAssembly;
整個過程分為5個步驟,它們已用注釋標識出來了,這里不再重復了。
如何調用編譯結果
前面的代碼把一段文本字符串編譯成了程序集,現在還有最后一個問題:如何調用編譯結果?
答案:有二種方法,
1. 直接調用方法。
2. 實例化程序集中的類型,以接口方式調用方法。
其實這二種方法都需要使用反射,用反射定位到要調用的類型和方法。
第一種方法要求在生成代碼時,生成的類名和方法名是明確的,在調用方法時,我們有二個選擇:
1. 用反射的方式調用(這里只是一次反射)。
2. 為方法生成委托(用上篇博客介紹的方法),然后基於委托調用。
第二種方法要求在生成代碼時,首先要定義一個接口,保證生成的代碼能實現指定的接口,
然而用反射找到要調用的類型名稱,用反射或者委托調用構造方法創建類型實例,最后基於接口去調用。
我們熟悉的ASPX頁面就是采用了這種方式來實現的。
這二種方法也可以這樣區分:
1. 如果生成的方法是靜態方法,應該選擇第一種方法。
2. 如果生成的方法是實例方法,那么選擇第二種方法是合理的。
對於前面的示例,我采用了第一種方法了,因為類名和方法名稱都是事先確定的而且實現起來比較簡單。
// 6. 找到目標方法,並調用 Type t = asm.GetType("OptimizeReflection.用戶手冊"); MethodInfo method = t.GetMethod("Main"); method.Invoke(null, null);
能不能不使用委托? 如何用好CodeDOM?
在這篇博客中我不知道把它們安排在哪里較為合適,算了,還是把答案留給下篇博客吧。
博客中所有代碼將在后續博客中給出。