反射概念在網上到處都有,但是講到的具體的應用很少,一個重要的原因是現實中真的很少用得到它。引用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
希望有幫助!