反射概念在網上到處都有,但是講到的具體的應用很少,一個重要的原因是現實中真的很少用得到它。引用msdn上對“反射”的解釋:
"通過 System.Reflection 名稱空間中的類以及 System.Type,您可以獲取有關已載入的程式集和在其中定義的型別(如類、介面和值型別)的資訊。 您也可以使用反射在執行時建立型別例項,以及呼叫和訪問這些例項。"
這個解釋著實讓人難以理解,至少對新手來講,一頭霧水。那麼這篇文章我首先從概念下手,用一種儘量易於理解的方式解釋一下反射到底是個什麼東西。文章最後附加一個“反射”應用的demo,它能監聽任何一個程式集中的任何一個Control(深度優先順序遍歷所有子控制元件)的所有事件資訊。
在程式開發階段,如果我們要使用一個型別(包括例項化該型別物件,訪問物件等等操作),分三個步驟:
- 新增包含了這個型別程式集的引用(.net自帶的型別程式集預設已引用);
- 程式碼中直接使用該型別(用一種文字字元的形式,比如型別名、方法名、屬性名);
- 編譯正確通過,程式執行。
圖1
以上是我們常用的開發步驟,幾乎不用去想為什麼要這樣,每個人都會。這個流程中第一個前提就是“要引用包含了這個型別的程式集”。假設某一次開發過程中,我們不能提前引用到包含這個型別的程式集(先不要否定這種情況,只能說明你沒碰到),那麼我們改怎麼寫程式碼?正常情況下,我們訪問A類是這樣的(假設A類在A.dll程式集中):
1 A a=new A(); 2 a.PropertyName=”123”; 3 a.EventName+=a_EventName; 4 a.DoSomething();
正常編譯通過。但是現在我們沒有引用A.dll,我們改怎麼寫程式碼?還是像上面那樣寫嗎?不對,因為編譯通不過,編譯器會提示“缺少對程式集的引用”(這個很容易理解,因為你沒有引用程式集,編譯器肯定不會知道)。
以上就是我們會碰到的一種情況,即:
在有些時候,我們可能會使用一種資料型別,但是開發階段並不能引用到包含該型別的程式集,包含了這個型別的程式集只能在程式執行起來之後,動態的引用進來。開發階段引用程式集如果叫“靜態引用程式集”,那麼執行時引用程式集就應該叫“動態引用程式集”了。後者會造成一個問題:我們該怎麼寫程式碼去訪問動態引用程式集中的型別?
圖2
這個時候,反射的作用就出來了。反射能夠讓我們使用在編譯階段編譯器不知道的資料型別(注意是編譯器不知道,不是我們不知道)。再舉前面使用A型別的例子,我們在開發階段沒有引用A.dll程式集,因此下面的程式碼無法通過編譯:
1 A a=new A(); 2 a.PropertyName=”123”; 3 a.EventName+=a_EventName; 4 a.DoSomething();
但是,我們知道程式執行之後,可以動態引用到A.dll,那麼現在程式碼中怎麼使用A型別呢?看下面的程式碼:
1 Assembly assembly=Assembly.LoadFile(“C:\\A.dll”); //動態引用A.dll 2 Type t = assembly.GetType(“ReflectionTestNS.A”); //獲取A型別在程式集中的資訊 3 object oj=Activator.CreateInstance(t); //類似new A() 4 PropertyInfo p = t.GetProperty("PropertyName"); 5 if (p != null) 6 { 7 p.SetValue(oj, “123”, null); //類似oj.PropertyName=”123” 8 } 9 EventInfo ei = t.GetEvent("EventName"); 10 if (ei != null) 11 { 12 Type tt = ei.EventHandlerType; 13 ei.AddEventHandler(oj, Delegate.CreateDelegate(tt, this, "oj_EventName")); 14 //類似oj.EventName+=oj_EventName 15 } 16 MethodInfo m = t.GetMethod("DoSomething", new Type[] { }); 17 if (m != null) 18 { 19 m.Invoke(oj, null); //類似oj.DoSomething() 20 }
上面程式碼能夠通過編譯,我們可以通過以上程式碼去訪問A.dll程式集中的A型別,即使在開發階段我們沒有A.dll的引用。需要注意的幾點有:
1)雖然我們在開發階段不能引用到A.dll程式集,但是我們應該對A.dll中的型別有了解,知道名稱空間,知道資料型別名稱,知道方法名稱引數型別,知道事件名稱委託型別等等,也就是說,雖然編譯器不知道A.dll中的型別資訊,我們開發人員必須知道A.dll中的型別資訊,這樣以來,我們才能利用“反射”加上“文字字元”作為標示去訪問這個型別。
2)1)中規定的開發人員必須瞭解A.dll中的型別資訊,僅僅是當你需要詳細的使用一個型別物件時,如果你只需要獲取A.dll中的有哪些型別、每個型別有哪些方法引數屬性事件等,然後將他們的資訊顯示出來,完全沒必要知道A.dll中的型別資訊,比如VS中編輯器的智慧提示功能,或者Reflector等利用反射實現資料集中型別資訊顯示的軟體,它們的開發人員知道你的程式集資訊嗎?不知道,但是還是能工作很好。但是就上面“使用A型別”的例子來講,你必須知道A.dll中的A型別中有個叫EventName的事件,你才能給它的物件註冊事件,否則可以說你根本使用不了A型別的物件。
3)編譯階段,物件和方法就可以關聯起來(比如a.DoSomething()能通過編譯),這種如果稱之為“早期繫結”(early binding),那麼通過反射將物件和方法關聯起來就稱為“晚期繫結”(late binding)。前者在編譯階段編譯器可以檢查正確性,後者編譯器無能為力,因為編譯器不知道A.dll的任何資訊。
一張圖區分兩種訪問程式集中型別的區別:
圖3
個人認為,正常開發中用不到反射,所以儘量避免使用反射(反射有缺陷,執行效能編譯階段不能檢查正確性等),本系列部落格(十七)中講到的擴充套件應用程式,就使用到了反射,文中指出將外掛打包成dll程式集後,放入宿主程式的plugins目錄中,宿主程式啟動後,會動態引用plugins目錄中的程式集,動態建立外掛型別例項,然後訪問它。那麼如果你是宿主程式的開發人員,你會在開發階段引用到第三方開發的外掛程式集dll檔案嗎?不能,但是你還是得在程式碼中使用它的型別。
注:上面擴充套件應用程式中不全使用反射去訪問動態引用程式集中的型別,因為它使用到了一個IPlugin的介面,動態例項化外掛物件後,是使用IPlugin介面引用這個物件,之後所有的都是通過這個介面去訪問物件(之後沒有使用到反射),它避免了使用反射的效能問題和在編譯階段能夠檢查程式的正確性(開發階段宿主程式能夠引用IPlugin介面程式集),這個也是必須使用反射場合的一種改進,後續有機會我會詳細說明。
另外網上有很多講述反射的文章,都是用類似如下程式碼作為反射應用例項,
1 void btn1_Click(object sender,EventArgs e) 2 { 3 Type t = typeof(Button); 4 //或者 5 Type t = btn1.GetType(); 6 PropertyInfo p = t.GetProperty(“Text”); 7 if(p!=null) 8 { 9 p.SetValue(btn1,”123”,null); //利用反射編輯btn1的Text屬性 10 } 11 }
以上類似程式碼並沒有錯誤,只是我覺得會給人誤導,反射的真正使用場合不在這裡(這裡完全用不著,為什麼不直接使用btn1.Text=”123”呢?),看多了,人們就會認為反射就是這作用,用在這裡。
Demo中包含了兩個專案,一個是簡單的說明了正常方法使用BackgroundWorker這個型別,和動態引用程式集動態建立BackgroundWorker型別物件(假裝開發階段沒有引用包含BackgroundWorker型別的程式集),兩者的區別。另一個專案能夠動態引用程式集,並且動態例項化Control類例項,關鍵還能監聽任何控制元件的所有事件,然後輸出事件資訊,這個有點複雜,不僅僅使用到了System.Reflection名稱空間中的型別,還用了System.Reflection.Emit名稱空間中的型別,後者可以動態建立型別,由於每個控制元件的每個事件型別不一樣,並且個數還不確定,所以我們沒有辦法事先定義一個通用的事件註冊者,只能挨個為每個事件動態建立一個事件註冊者類。第二個專案流程見下圖:
圖4
第二個專案參見了CodeProject上老外的一篇文章(http://www.codeproject.com/Articles/3317/ControlInspector-monitor-Windows-Forms-events-as-t),註釋請參見我的,程式碼中有詳細的中文解釋。
Demo截圖:
圖5 靜態引用程式集訪問型別 和 動態引用程式集訪問型別的區別
圖6 反射應用
總之,反射能夠讓你使用在編譯階段還不可達的程式集(型別)。
原始碼下載地址:http://files.cnblogs.com/xiaozhi_5638/ReflectionTest.rar
希望有幫助!