1、麻煩前的寧靜:數組 「老趙,嗯,幫忙測試一下這個方法。」唉,同伴傳過來一個託管dll文件。唉,真麻煩啊,爲何不用CVS呢?用個VSS也好啊。老趙一邊抱怨着一邊打開了VS.Net 2003。安全 測試嘛,總免不了使用NUnit。想到NUnit的藝術性,嘖嘖,老趙總要讚歎一番,看着「環保」的綠色就讓人有一種心情愉快的感受。框架 須要測試的代碼以下,須要測試TestMethod( )方法 : namespace TestAssembly { public class TestClass { public TestClass(){} public double TestMethod( double param ) { return param * 0.75; } } }ide 在項目中引入dll文件,三下兩下就寫完了,測試代碼以下: [Test] public void Test() { TestAssembly.TestClass tc = new TestAssembly.TestClass(); Assert.AreEqual( 0.75, tc.TestMethod(1.0) ); Assert.AreEqual( 7.5, tc.TestMethod(10.0) ); }性能 綠燈,經過,任務完成。測試 2、麻煩的開始:ui 「老趙,程序集更新了,再測試一下」。將舊的文件替換成新的,運行Unit,紅燈。可是一看錯誤:「找不到方法:Double TestAssembly.TestClass.TestMethod(Double)」。怎麼會這樣?回到VS.Net 2003,Ctrl+Alt+B,果真,編譯不經過:TestAssembly.TestClass」並不包含對「TestMethod」的定義。spa 突然QQ上頭像閃動:「差點忘了告訴你,方法名改爲NewTestMethod了。」 那麼還有什麼好說的呢?修改測試代碼唄。插件 QQ上又來消息了:「對了,方法名我隨時會改,‘測試框架’留大一些啊。」老趙頓時感到一陣胸悶。這個所謂的「測試框架」是什麼東西,怎麼作?算了,老趙是個體諒同伴的好同志:「唉唉,那麼你總要告訴我測試哪一個方法吧。」繼承 「之後我附帶一個文本文件給你,裏面寫着測試哪一個方法。」 不一下子,又傳來一個txt文件,打開一看,裏面寫着:「TestAssembly.TestClass.NewTestMethod」。老趙長嘆一聲:「沒想到在測試時竟然也會用到反射……」 不過還好,也不復雜,老趙開始動手修改代碼: [Test] public void Test() { Assembly asm = Assembly.LoadFrom("TestAssembly.dll"); //省略類名和方法名的得到代碼 string className = "TestAssembly.TestClass"; string methodName = "NewTestMethod"; Type classType = asm.GetType(className, true); object obj = classType.InvokeMember(null, BindingFlags.CreateInstance, null, null, null); object result = classType.InvokeMember( methodName, BindingFlags.InvokeMethod, null, obj, new object[]{1} ); Assert.AreEqual( 0.75, (double)result ); } 運行NUnit,綠燈,放行。 3、麻煩無極限: 「這是我新寫的一個方法,測試n遍吧,看看返回值是否在這個範圍內。對了,方法名仍是寫在文本文 「n大概爲多少?」 「越多越好,就幾十萬遍吧,嘿嘿。」 老趙根本沒有多想,C&P,再隨手改寫了一點代碼就完成了: [Test] public void Test() { //…… for (int i = 0; i < 500; i++) { for (int j = 0; j < 500; j++) { object result = classType.InvokeMember( methodName, BindingFlags.InvokeMethod, null, obj, new object[]{1} ); //…… } } }
打開NUnit,運行。嗯?怎麼沒反應?機器也處於瀕死狀態。打開「任務管理器」,「nunit-gui」佔用了幾乎全部的CPU。突然想到了Reflection對於性能的影響。
老趙連忙在QQ上呼叫:「在嗎?商量一下事情。」「什麼事情?」
「我給你個類你派生一下,override它的virtual方法,用Reflection太慢」。 「什麼Reflection……不懂。並且我這個類已經有超類了。」 「汗……那麼我給你一個接口你實現一下如何?」 「那麼我不是沒法改方法名了?」
老趙突然也意識到了這一點,匆匆告別同伴,尋找其它解決辦法。 「唉,想偷懶一下的,仍是沒有辦法,算了,只能這樣了。」老趙自言自語。
方法其實也不難,只是須要寫個delegate,而後把要運行的方法綁定上去便可。這麼作和派生一個現成的類和實現一個現成接口相比要略微麻煩一些,並且執行速度也略慢一些。
「無論怎麼樣,動手吧。」老趙心想。
先定義一個委託,而後開始寫代碼: delegate double MyDelegate(double d); [Test] public void Test() { Assembly asm = Assembly.LoadFrom("TestAssembly.dll"); //省略類名和方法名的得到代碼 string className = "TestAssembly.TestClass"; string methodName = "NewTestMethod"; Type classType = asm.GetType(className, true); object target = classType.InvokeMember(null, BindingFlags.CreateInstance, null, null, null); object target = classType.InvokeMember( methodName, BindingFlags.InvokeMethod, null, obj, new object[]{1} ); MyDelegate run = (MyDelegate)Delegate.CreateDelegate( typeof(MyDelegate), target, methodName); for (int i = 0; i < 500; i++) { for (int j = 0; j < 500; j++) { object result = run(1.0); //…… } } } 結果如何?幾乎是瞬間運行就結束了,其中性能差距何等明顯! 老趙長吁一口氣,真的沒有料想到會遇到如此莫名其妙的測試方式。不知道之後還會怎麼樣,老趙的麻煩看來是少不了了…… 4、麻煩後的總結: .Net的反射機制至關強,雖然還不能徹底獲得全部的程序集信息,可是對於幾乎全部的應用都足夠了。反射機制的原理是從程序集的元數據中獲取各類信息,元數據保存在程序集的各類「表格」裏。反射機制對於編寫動態加載的程序很是重要,好比插件程序。 在編譯時就肯定的成員訪問來天然效率最高,經過反射機制來訪問一個成員效率就低下了。就拿InvokeMember方法來說,要肯定訪問的是哪一個成員,首先要遍歷整張表,而且要作大量的字符串匹配;其次,再傳遞參數時,咱們會首先構造一個數組,初始化其中的元素,而後再交給反射方法調用。在調用時,反射機制還用從數組中提取參數壓入堆棧。相反,在編譯時就被調用的代碼,在call一個方法時,會將實例的引用和第一個參數放在寄存器中,這樣又減小的訪存操做,運行效率天然就提升了。再次,反射機制在執行方法前必須還要檢測參數的數量和類型是否正確。最後還有比較關鍵一點,並非程序集中全部的成員都能被訪問,CLR要經過檢查安全許可來肯定此次訪問是否合法。以上四點致使了在反射機制中的效率低下。 所以,InvokeMember方法雖然強大,可是若是要頻繁使用一個方法,就應該使用別的方法來訪問成員。 要提升反射機制的效率,很天然想到的方法就是減小上述四項的使用。事實上,一些提升反射機制運行效率的技巧,就是在這一點上作文章。 下面就會介紹一寫提升反射機制效率的方法,並將它們與直接運行和InvokeMember的效率進行量化的比較,直觀地反映出這些技巧的重要性。 仍是以原來的例子進行改編,添加一個接口和委託供使用,原來的例子代碼以下: namespace TestAssembly { public class TestClass: ITestInterface { public TestClass(){} public double TestMethod( double param ) { return param * 0.75; } } public static void Main(string[] args) { int n = 1000; Test1(n);//直接調用 Test2(n);//經過InvokeMember調用 Test3(n);//經過接口調用 Test4(n);//綁定至delegate Console.In.ReadLine(); } public interface ITestInterface { double TestMethod( double param ); } public delegate double TestDelegate( double param ); } 首先,寫代碼測量直接運行的效率,代碼以下: static void Test1(int n) { TestClass tc = new TestClass(); DateTime startTime = DateTime.Now; for (int i = 0; i < n; i++) for (int j = 0; j < n; j++) tc.TestMethod( 1.0 ); TimeSpan ts = DateTime.Now - startTime; Console.Out.WriteLine( "Test1: " + ts ); } 接着是經過InvokeMember調用,代碼以下: static void Test2(int n) { Type testType = typeof(TestClass); object obj = testType.InvokeMember(null, BindingFlags.CreateInstance, null, null, null); DateTime startTime = DateTime.Now; for (int i = 0; i < n; i++) for (int j = 0; j < n; j++) testType.InvokeMember( "TestMethod", BindingFlags.InvokeMethod, null, obj, new object[]{1.0} ); TimeSpan ts = DateTime.Now - startTime; Console.Out.WriteLine( "Test2: " + ts ); 而後,是將得到的object用接口來引用,而後調用方法,代碼以下: static void Test3(int n) { Type testType = typeof(TestClass); object obj = testType.InvokeMember(null, BindingFlags.CreateInstance, null, null, null); ITestInterface instance = (ITestInterface)obj; DateTime startTime = DateTime.Now; for (int i = 0; i < n; i++) for (int j = 0; j < n; j++) instance.TestMethod(1.0); TimeSpan ts = DateTime.Now - startTime; Console.Out.WriteLine( "Test3: " + ts ); } 最後,綁定至一個delegate,代碼以下: static void Test4(int n) { Type testType = typeof(TestClass); object obj = testType.InvokeMember(null, BindingFlags.CreateInstance, null, null, null); TestDelegate testMethod = (TestDelegate)Delegate.CreateDelegate( typeof(TestDelegate), obj, "TestMethod" ); DateTime startTime = DateTime.Now; for (int i = 0; i < n; i++) for (int j = 0; j < n; j++) testMethod(1.0); TimeSpan ts = DateTime.Now - startTime; Console.Out.WriteLine( "Test4: " + ts ); } 如下是輸出結果: Test1: 00:00:00.0200288 Test2: 00:00:01.6824192 Test3: 00:00:00.0200288 Test4: 00:00:00.0300432 其時間的絕對值由於和機器性能相關,所以沒有任何意義。 Test1和Test3的測試結果相同,可見這是使用反射機制的最好方式。另外,文章一開始提到的,讓程序集派生於一個類,覆蓋其虛方法,而後在程序中經過超類的引用來調用子類的方法。可是因爲.Net不支持多繼承,所以使用這個方法就帶有必定的侷限性。使用派生與接口和類的方法都是編寫插件程序的經常使用方法,尤爲是接口方式。當咱們建立可擴展的應用程序時,接口應該處於中心位置。通常來講,應該建立一個程序集,在其中定義接口,接口即做爲應用程序和插件之間的通訊。而後保持這個程序集不變,而在本身的應用程序中則能夠任意修改,由於根本不會影響到插件程序。 Test2的運行時間使人不敢恭維,在屢次測試中均爲直接運行的幾十倍,可見InvokeMember方法並不適合大規模使用,並且因爲其參數繁多,對於方法的使用也很是複雜。在n越大時,差距愈發明顯。 Test4把方法綁定到了一個簽名相同的委託上,這也是個比較出色的方法。雖然理論上這樣的方法性能不如Test1,可是在測試中發現,n等於500時其效率還沒法和Test1區分開來。事實上這個方法被普遍使用,每每配合定製屬性會有很是使人信服的表現。 其實,Test4就使用了「一次綁定,屢次執行」的方法。其實,若是要訪問的並非方法而是其他類型的成員,使用各Info類也是比較經常使用的辦法。不過,綁定到這些類,更多的可能並非爲了執行,而是爲了查看其元數據屬性等其它目的。由於這樣的綁定,和綁定到delegate相比起來,不是很完全,在運行的效率上也不如delegate。 |