本章主要討論在編譯時對一個類型一無所知的狀況下,如何在運行時發現類型的信息、建立類型的實例以及訪問類型的成員。可利用本章講述的內容建立動態可擴展應用程序。web
反射使用的典型場景通常是由一家公司建立宿主應用程序,其餘公司建立加載項(add-in)來擴展宿主應用程序。宿主不能基於一些具體的加載項來構建和測試,由於加載項由不一樣公司建立,並且極有多是在宿主應用程序發佈以後才建立的。編程
咱們知道,JIT編譯器將方法的IL代碼編譯成本機代碼時,會查看il代碼中引用了哪些類型。在運行時,jit編譯器利用程序集的TypeRef和AssemblyRef元數據表來肯定哪個程序集定義了所引用的類型。在AssemblyRef元數據表的記錄項中,包含了構成程序集強名稱的各個部分。jit編譯器獲取全部這些部分—包括名稱、版本、語言文化和公鑰信息(public key token)--並把它們鏈接成一個字符串。而後,jit編譯器嘗試將與該標識匹配的程序集加載到AppDomain中(若是尚未加載的話)。若是被加載的程序集是弱命名的,那麼表示中就只包含程序集的名稱。c#
在內部,clr使用system.reflection.Assembly類的靜態load方法嘗試加載這個程序集。該方法在.net sdk文檔中時公開的,可調用它顯式地將程序集加載到AppDomain中。windows
在內部,Lad致使clr向程序集應用一個版本綁定重定向策略,並在Gac(全局程序集緩存)中查找程序集。若是沒找到,就接着去應用程序的基目錄、私有路徑子目錄和codebase位置查找。若是調用Load時傳遞的是弱命名程序集,load就不會想程序集應用版本綁定重定向策略,clr也不會去gac查找程序集。若是load找到指定的程序集,會返回對錶明已加載的那個程序集的一個Assembly對象的引用。若是沒找到,會拋出異常。api
在大多數動態可擴展應用程序中,Assembly的Load的方法是將程序集加載到AppDomain的首選方式。但它要求實現掌握構成程序集標識的各個部分。開發人員常常須要寫一些工具或實用程序來操做程序集,他們都要獲取引用了程序集文件路徑名(包括文件擴展名)的命令行實參。數組
調用Assembly的LoadFrom方法加載指定了路徑名的程序集:緩存
public class Assembly{ public static Assembly LoadFrom(string path); }
在內部,LoadFrom首先調用System.Reflection.AssemblyName類的靜態GetAssemblyName方法。該方法打開指定的文件,找到AssemblyRef元數據表的記錄項,提取程序集標識信息,而後以一個system.reflection.assemblyName對象的形式返回這些信息。隨後,LoadFrom方法在內部調用Assembly的Load方法,將AssemblyName對象傳給它。而後,clr應用版本綁定重定向策略,並在各個位置查找匹配的程序集。Load找到匹配程序集會加載它,並返回待辦已加載程序集的Assembly對象;LoadFrom方法將返回到這個值。若是Load沒有找到匹配的程序集,LoadFrom會加載經過LoadFrom的實參傳遞的路徑中的程序集。固然,若是已加載具備相同標識的程序集,LoadFrom方法就會直接返回表明已加載程序集的Assembly對象。安全
LoadForm方法容許傳遞一個URL做爲實參,以下:app
Assembly a=Assembly.LoadFrom(@」http://xxxxxxxxx.xxxxAssembly.dll」);dom
若是傳遞的是一個internet位置,clr會下載文件,把它安裝到用戶的下載緩存中,再從那兒加載文件。注意,當前必須聯網,不然會拋出異常。但若是文件以前已下載過,並且ie被設置爲脫機工做,就會使用之前下載的文件。
VS的Ui設計人員和其餘工具通常用的是Assembly的LoadFile方法。這個方法可從任意路徑加載程序集,並且能夠將具備相同標識的程序集屢次加載到一個AppDomain中。在設計器中對應用程序的ui進行修改,並且用戶從新生產了程序集時,便有可能發生這種狀況。經過LoadFile加載程序集時,clr不會自動解析任何依賴性問題;你的代碼必須向AppDomain的AssemblyResolve事件等級,並讓事件回調方法顯式地加載加載任何依賴的程序集。
若是你構建的一個工具只想經過反射來分析程序集的元數據,並但願確保程序集中的任何代碼都不會執行,那麼加載程序集的最佳方式就是使用Assembly的ReflectionOnlyLoadFrom方法或者使用Assembly的ReflectionOnlyLoad方法。
ReflectionOnlyLoadFrom方法加載由路徑指定的文件;文件的強名稱標識不會獲取,也不會在GAC和其餘位置搜索文件。ReflectionOnlyLoad方法會在GAC、應用程序基目錄、私有路徑和codebase指定的位置搜索指定的程序集。但和load方法不一樣的是,ReflectionOnlyLoad方法不會應用版本控制策略,因此你指定的是哪一個版本,得到的就是哪一個版本。要自行向程序集標識應用版本控制策略,可將字符串傳給AppDomain的ApplyPolicy方法。
利用反射來分析由這兩個方法之一加載的程序集時,代碼常常須要向AppDomain的ReflectionOnlyAssemblyResovle事件註冊一個回調方法,以便手動加載任何引用的程序集;clr不會自動幫你作這個事情。回調方法被調用時,它必須調用Assembly的ReflectionOnlyLoadFrom或ReflectionOnlyLoad方法來顯式加載引用程序集,並返回對程序集的引用。
注意:進程有人問到程序集卸載的問題。遺憾的是,clr不提供卸載單獨程序集的能力。若是clr容許這樣作,那麼一旦線程從某個方法返回至已卸載的一個程序集的代碼,應用程序就會崩潰。健壯性和安全性是clr最優先考慮的目標,若是容許應用程序以這樣的一種方式崩潰,就和它的設計初衷背道而馳了。卸載程序集必須卸載包含它的整個AppDomain。
使用ReflectionOnlyLoadFrom或ReflectionOnlyLoad方法加載的程序集表面上是能夠卸載的。畢竟,這些程序集中的代碼是不容許執行的。但CLR同樣不容許卸載用這兩個方法加載的程序集。由於用這兩個方法加載了程序集以後,仍然能夠利用反射來建立對象,以便引用這些程序集中定義的元數據。
許多應用程序都是由一個要依賴於衆多dll文件的exe文件構成。部署應用程序時,全部文件都必須部署。但有一個技術容許只部署一個exe文件。首先標識出exe文件要依賴的、不是做爲.NET Framework一部分不發的全部dll文件。而後將這些dll添加到vs項目中。對於添加的每一個dll,都顯式它的屬性,將它的「生成操做」更改成「嵌入的資源」。這回致使C#編譯器將dll文件嵌入exe文件中,之後就只須要部署這個exe。
在運行時,clr會找不到依賴的dll程序集。爲了解決這個問題,當應用程序初始化時,向AppDomain的ResolveAssembly事件登記一個回調方法,代碼大體以下:
private static Assembly ResolveEventHandler(object sender,ResolveEventArgs args) { string dllName=new AssemblyName(args.Name).Name+".dll"; var assem = Assembly.GetExecutingAssembly(); string resourceName = assem.GetManifestResourceNames().FirstOrDefault(c => c.EndsWith(dllName)); if (resourceName==null) { return null;//not found,maybe another handler will find it } using (var stream=assem.GetManifestResourceStream(resourceName)) { byte[] assemblyData=new byte[stream.Length]; stream.Read(assemblyData, 0, assemblyData.Length); return Assembly.Load(assemblyData); } }
如今,線程首次調用一個方法時,若是發現該方法引用了依賴DLL文件中的類型,就會引起一個AssemblyResolve事件,而上述回調代碼會找到所需的簽入dll資源,並調用assembly的load方法獲取一個byte[]實參的重載版原本加載所需的資源。雖然我喜歡將依賴dll嵌入程序集的技術,但要注意這會增大應用程序在運行時的內存消耗。
總所周知,元數據時用一系列的表存儲的。生成程序集或模塊時,編譯器會建立一個類型定義表、一個字段定義表、一個方法定義表以及其餘表。利用system.reflection命名空間中包含的類型,能夠寫代碼來反射這些元數據表。實際上,這個命名空間中的類型爲程序集或模塊中包含的元數據提供了一個對象模型。
利用對象模型中的類型,能夠輕鬆枚舉類型定義元數據表中的全部類型,而針對每一個類型均可獲取它的基類型、它實現的接口以及與類型關聯的標誌。利用system.reflection命名空間中的其餘類型,還可解析對應的元數據表來查詢類型的字段、方法、屬性和事件。還可發現應用於任何元數據實體的定製特性。甚至有些類容許判斷引用的程序集;還有一些方法能返回一個方法的il字節流。利用全部這些信息,很容易構建出與Microsoft的ilDasm.exe類似的工具。
事實上,只有極少數應用程序才須要使用反射類型。若是類庫須要理解類型的定義才能提供豐富的功能,就適合使用反射。例如,fcl的序列化機制就是利用反射來判斷類型定義了哪些字段。而後,序列化格式器(serialiazation formatter)可獲取這些字段的值,把它們寫入字節流以便經過internet傳送、保存到文件或複製到剪貼板。相似地,在設計期間,microsoft visual studio設計器在web窗體或windows窗體上放置控件時,也利用反射來決定要向開發人員顯示的屬性。
在運行時,當應用程序須要從特定程序集中加載特定類型以執行特定任務時,也要使用反射。例如,應用程序可要求用戶提供程序集和類型名。而後應用程序可顯式加載程序集,構造類型的實例,再調用類型中定義的方法。以這種方式綁定到類型並調用方法稱爲晚期綁定。(對應的,早期綁定是指在編譯時就肯定應用程序要使用的類型和方法)。
反射是至關強大的機制,容許在運行時發現並使用編譯時還不瞭解的類型及成員。可是,他也有下面兩個缺點。
1 反射形成編譯時沒法保證類型安全性。因爲反射嚴重依賴字符串,因此會喪失編譯時的類型安全性。例如,執行type.getType(「int」);要求經過反射在程序集中查找名爲int的類型,代碼會經過編譯,但在運行時會返回null,由於clr只知道system.int32,不知道int。
2 反射速度慢。使用反射時,類型及其成員的名稱在編譯時未知;你要用字符串名稱標識每一個類型及成員,而後再運行時發現它們。也就是說,使用system.reflection命名空間中的類型掃描程序集的元數據時,反射機制會不停執行字符串搜索。一般,字符串搜索執行的是不區分大小寫的比較,這回進一步影響速度。
使用反射調用成員也會影響性能。用反射調用方法時,首先必須將實參打包成數組;在內部,反射必須將這些實參解包到線程棧上。此外,在調用方法前,clr必須檢查實參具備正確的數據類型。最後,clr必須確保調用者有證券的安全權限來訪問被調用成員。
基於上市全部緣由,最好避免利用反射來訪問字段或調用方法/屬性。應該利用如下兩種技術之一開發應用程序來動態發現和構造類型實例。
1 讓類型從編譯時已知的基類型派生。在運行時構造派生類型的實例,將對它的引用放到基類型的變量中,再調用基類型定義的虛方法。
2 讓類型實現編譯時已知的接口。在運行時構造類型的實例,將對它的引用放到接口類型的變量中,再調用接口定義的方法。
在這兩種技術中,我我的更喜歡使用接口技術而非基類技術,由於基類技術不容許開發人員選擇特定狀況下工做得最好的基類。不過,須要版本控制的時候基類技術更合適,由於可隨時向基類添加成員,派生類會直接繼承該成員。相反,要向接口添加成員,實現該接口的全部類型都得修改它們的代碼並從新編譯。
反射常常用於判斷程序集定義了哪些類型。Fcl提供了許多api來獲取這方面的信息。目前經常使用的是assembl的exportedTypes屬性。
static void Main(string[] args) { string dataAssembly = "System.Data,version=4.0.0.0," + "culture=neutral,PublicKeyToken=b77a5c561934e089"; LoadAssemAndShowPublicTypes(dataAssembly); } private static void LoadAssemAndShowPublicTypes(string assemblyName) { //顯式地將程序集加載到這個appDomain中 Assembly a = Assembly.Load(assemblyName); //在一個循環中顯示已加載程序集中每一個公開導出type全面 foreach (Type t in a.ExportedTypes) { Console.WriteLine(t.FullName); } }
注意,上述代碼遍歷system.type對象構成的數組。system.type類型是執行類型和對象操做的起點。system.type對象表明一個類型引用(而不是類型定義)。
總所周知,system.object定義了公共非虛實例方法getType。調用這個方法時,clr會斷定指定對象的類型,並返回對該類型的type對象的引用。因爲在一個appDomain中,每一個類型只有一個type對象,因此可使用相等和不相等操做符來判斷兩個對象是否是相同的類型。
除了調用object的getType方法,fcl還提供了得到type對象的其餘幾種方式。
1 system.type類型提供了靜態getType方法的幾個重載版本。全部版本都接受一個string參數。字符串必須指定類型的全名。
2 system.typeinfo類型提供了實例成員DeclaredNestedTypes和GetDeclaredNestedType。
3 system.reflection.assembly類型提供了實例成員getType,definedtypes和exportedTypes。
許多編程語言都容許使用一個操做符並根據編譯時已知的類型名來得到type對象。儘可能用這個草莝夫獲取type引用,而不要使用上述列表中的任何方法,由於操做符生成的代碼通暢更快。C#的這個操做符稱爲typeof,一般用它將晚期綁定的類型信息與早期綁定(編譯時已知)的類型信息進行比較。
private static void SomeMethod(object o) { //getType在運行時返回對象的類型(晚期綁定) //typeof返回指定類的類型(早期綁定) if (o.GetType()==typeof(FileInfo)) { //..... } if (o.GetType()==typeof(DirectoryInfo)) { //..... } }
上述代碼的第一個if語句檢查變量o是否引用了fileInfo類型的對象;它不檢查o是否引用從fileInfo類型派生的對象。換而言之,上述代碼測試的是精確匹配,而非兼容匹配。(使用轉型或c#的is/as操做符時,測試的就是兼容匹配)。
如前所述,type對象是輕量級的對象引用。要更多地瞭解類型自己,必須獲取一個typeinfo對象,後者才表明類型定義。可調用system.reflection.introspectionExtensions的getTypeinfo擴展方法將Type對象轉換成typeinfo對象。
Type typeReference=…;//例如o.gettype()或者typeof(Object) TypeInfo typeDefinition=typeReference.getTypeInfo();=
另外,雖然做用不大,但還可調用TypeInfo的AsType方法將TypeInfo對象轉換爲Type對象。
TypeInfo typeDefinition=……; Type typeReference = typeDefinition.AsType();
獲取typeInfo對象會強迫clr確保已加載類型的定義程序集,從而對類型進行解析。這個操做可能代價高昂。若是隻須要類型引用(type對象),就應該避免這個操做。但一旦得到了typeInfo對象,就可查詢類型的許多屬性進一步瞭解它。大多數屬性,好比IsPublic,isSealed,isAbstract,isClass和isValueType等,都指明瞭與類型關聯的標誌。另外一些屬性,好比assembly,assemblyQualifiedName,fullName和module等,則返回定義該類型程序集或模塊的名稱以及類型全名。還可查詢baseType屬性來獲取對類型的基類型的引用。除此以外,還有許多方法能提供關於類型的更多信息。
如下代碼使用本章討論的許多概念將一組程序集加載到Appdomain中,並顯示最終從System.exception派生的全部類。
private static void Go() { //顯示加載想要反射的程序集 LoadAssemblies(); //對全部類型進行篩選和排序 var allTypes = (from a in AppDomain.CurrentDomain.GetAssemblies() from t in a.ExportedTypes where typeof(Exception).GetTypeInfo().IsAssignableFrom(t.GetTypeInfo()) orderby t.Name select t).ToArray(); //生成並顯示繼承層次結構 Console.WriteLine(WalkInheritanceHierarchy(new StringBuilder(),0,typeof(Exception),allTypes )); } private static StringBuilder WalkInheritanceHierarchy(StringBuilder sb ,int indent,Type baseType,IEnumerable<Type> allTypes) { string spaces = new String(' ', indent * 3); sb.AppendLine(spaces + baseType.FullName); foreach (var t in allTypes) { if (t.GetTypeInfo().BaseType!=baseType) { continue; } WalkInheritanceHierarchy(sb, indent + 1, t, allTypes); } return sb; } private static void LoadAssemblies() { string[] assemblies = {"System,PublicKeyToken={0}", "System.Core,PublicKeyToken={0}","System.Data,PublicKeyToken={0}","System.Design,PublicKeyToken={1}"}; string ecmaPublicKeyToken = "b77a5c561934e089"; string msPublicKeyToken = "b03f5f7f11d50a3a"; //獲取包含system.object的程序集的版本,假定其餘全部程序集都是相同的版本 Version version = typeof(System.Object).Assembly.GetName().Version; //顯示加載想要反射的程序集 foreach (var a in assemblies) { string assemblyIdentity = string.Format(a, ecmaPublicKeyToken, msPublicKeyToken) + ",Culture=neutral,Version=" + version; Assembly.Load(assemblyIdentity); } }
輸出以下
獲取對type派生對象的引用以後,就能夠構造該類型的實例了。fcl提供了一下幾個機制。
1 system.activator的createInstance方法
activator類提供了靜態createInstance方法的幾個重載版本。調用方法時既可傳遞一個type對象引用,也可傳遞標識了類型的string。直接獲取類型對象的幾個版本較爲簡單。你要爲類型的構造器傳遞一組實參,方法返回新對象的引用。
用字符串來制定類型的幾個版本稍微複雜一些。首先必須指定另外一個字符串來表示定義了類型的程序集。其次,若是正確配置了遠程訪問選項,這些方法還容許構造遠程對象。
2 system.activator的createInstanceForm方法
activator類還提供了一組靜態createInstanceForm方法,他們與createInstance的行爲類似,只是必須經過字符串參數來指定類型及其程序集。程序集用assembly的loadForm(而非load)方法加載到調用appDin中。因爲都不接受type參數,因此返回的都是一個objectHandle對象引用,必須調用ObjectHandle的unwrap方法進行具體化。
3 system.appdomain的方法
appdomain類型提供了4個用於構造類型實例的實例方法,包括createInstance,createInstanceFrom和createInstanceFromAndUnwrap。這些方法和行爲和activator類的方法類似。區別在於他們都是實例方法,容許指定在哪一個appdomain中構造對象。另外,帶unwrap後綴的方法還能簡化操做,沒必要執行額外的方法調用。
4 system.reflection.constructorInfo的invoke實例方法
使用一個type對象引用,能夠綁定到一個特定的構造器,並獲取對構造器的constructorInfo對象的引用。而後,可利用constructorInfo對象引用來調用它的invoke方法。類型老是在調用appdomain中建立,返回的是對新對象的引用。
注意: clr不要求值類型定義任何構造器。activator的createInstance方法容許在不調用構造器的狀況下建立值類型的實例。必須調用createInstance方法獲取單個type參數的版本或者獲取type和boolean參數的版本。
利用前面列出的機制,可爲除數組和委託以外的全部類型建立對象。建立數組須要調用array的靜態createInstance方法。全部版本的createInstance方法獲取的第一個參數都是對數組元素type的引用。createInstance的其餘參數容許指定數組位數維數和上下限的各類組合。建立委託則要調用methodInfo的靜態createDelegate方法。全部版本的createDelegate方法獲取的第一個參數都是對委託type的引用。createDelegate方法的其餘參數容許指定在調用實例方法時應將哪一個對象做爲this參數傳遞。
否早泛型類型的實例首先要獲取對開放類型的引用,而後調用type的MakeGenericType方法並向其傳遞一個數組(其中包含要做爲類型實參使用的類型)。而後,獲取返回的type對象並把它傳給上面列出的某個方法。
internal sealed class Dictionary<TKey,TValue>{} class Program { static void Main(string[] args) { //獲取對泛型類型的類型對象的引用 Type openType = typeof(Dictionary<,>); //使用Tkey=string、Tvalue=int封閉泛型類型 Type closedType = openType.MakeGenericType(typeof(string), typeof(int)); //構造封閉類型的實例 Object o = Activator.CreateInstance(closedType); //證明能正常工做 Console.WriteLine(o.GetType()); } }
運行結果
ConsoleApp2.Dictionary`2[System.String,System.Int32]
詳見原書,這塊我理解的不深。
到目前爲止,本章的重點一直都是構建動態可擴展應用程序所需的反射機制,包括程序集加載、類型發現以及對象構造。要得到好的性能和編譯時的類型安全性,應儘可能避免使用反射。若是是動態可擴展應用程序,構造好對象後,宿主代碼通常要將對象轉型爲編譯時已知的接口類型或者基類。這樣訪問對象的成員就能夠得到較好的新能,並且能夠確保編譯時的類型安全性。
本章剩餘部分將從其餘角度探討反射,目的是發現並調用類型的成員。通常利用這個功能建立開發工具和實用程序,查找特定編程模式或者對特定成員的使用,從而對程序集進行分析。例子包括ilDasm,visual studio的wpf設計器。另外,一些類庫也利用這個功能發現和調用類型的成員,爲開發人員提供便利和豐富的功能。
字段、構造器、方法、屬性、事件和嵌套類型均可以定義成類型的成員。fcl包含抽象基類system.reflection.memberInfo,封裝了全部類型成員都通用的一組屬性。
如下程序演示瞭如何查詢類型的成員並顯示成員的信息。代碼處理的是由調用AppDomain加載的全部程序集定義的全部公共類型。對每一個類型都調用DeclaredMembers屬性以返回由MemberInfo派生對象構成的集合:每一個對象都引用類型中定義的一個成員。而後,顯示每一個成員的種類(字段、構造器、方法和屬性等)及其字符串值。
static void Main(string[] args) { //遍歷這個appDomain中加載的全部程序集 Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); foreach (var a in assemblies) { Show(0,"Assembly:{0}",a); //查找程序集中的類型 foreach (var t in a.ExportedTypes) { Show(1,"Assembly:{0}",t); //發現類型的成員 foreach (var mi in t.GetTypeInfo().DeclaredMembers) { string typeName = string.Empty; if (mi is Type) { typeName = "(Nested) Type"; } if (mi is FieldInfo) { typeName = "FieldInfo"; } if (mi is MethodInfo) { typeName = "MethodInfo"; } if (mi is ConstructorInfo) { typeName = "ConstructorInfo"; } if (mi is PropertyInfo) { typeName = "PropertyInfo"; } if (mi is EventInfo) { typeName = "EventInfo"; } Show(2,"{0}:{1}",typeName,mi); } } } } private static void Show(int indent,string format,params object[] args) { Console.WriteLine(new string(' ',3*indent)+format,args); }
輸出結果
因爲memberInfo類是成員層次結構的根,因此有必要更深刻地研究一下它。下表展現了memberInfo類提供的幾個只讀屬性和方法。這些屬性和方法是一個類型的全部成員都通用的。不要忘了system.typeInfo從memberInfo派生。
在查詢declaredMembers屬性所返回的集合中,每一個元素都是對層次結構中的一個具體類型的引用。雖然TypeInfo的declaredMembers屬性能返回類型的全部成員,但還可利用TypeInfo提供的一些方法返回具備指定字符串名稱的成員類型。例如,利用TypeInfo的GetDeclaredNestedType、GetDeclaredField等
下圖總結了用於遍歷反射對象模型的各類類型。基於AppDomain,可發現其中加載的全部程序集。基於程序集,可發現構成它的全部模塊。基於程序集或模塊,可發現它定義的全部類型。基於類型,可發現它的嵌套類型、字段、構造器、方法、屬性和事件。命名空間不是這個層次結構的一部分,由於它們只是從語法角度將相關類型彙集到一塊兒。clr不知道什麼是命名空間。要列出程序集中定義的全部命名空間,需枚舉程序集中的全部類型,並查看其namespace屬性。
基於一個類型,還可發現它實現的接口。基於構造器、方法、屬性訪問器方法或者事件的添加、刪除方法,可調用GetParameters方法來獲取由parameterInfo對象構成的數組,從而瞭解成員的參數的類型。還可查詢只讀屬性ReturnParameter得到一個parameterInfo對象,他詳細描述了成員的返回類型。對於泛型類型或方法,可調用GetgenericArguments方法來得到類型參數的集合。最後,針對上述任何一項,均可查詢customAttributes屬性來得到應用於它們的自定義定製特性的集合。
發現類型定義的成員後可調用它們。
PropertyInfo類表明與屬性有關的元數據信息;也就是說,PropertyInfo提供了canRead、canWrite和PropertyType只讀屬性,他們指出屬性是否可讀和可寫,以及屬性的數據類型是什麼。PropertyInfo還提供了只讀getMethod和SetMethod屬性,他們返回待辦屬性get和set訪問器方法的MethodInfo對象。PropertyInfo的getValue和setValue方法只是爲了提供方便:在內部,,他們會本身調用合適的methodInfo對象。爲了支持有參屬性(c#的索引器),getValue和setValue方法提供了一個object[]類型的index參數。
EventInfo類型表明與事件有關的元數據信息。EventInfo類型提供了只讀EventHandlerType屬性,返回事件的基礎委託的type。EventInfo類型還提供了只讀addMethod和RemoveMethod屬性,返回爲事件增刪委託的方法的methodInfo對象。增刪委託可調用這些MethodInfo對象,也可調用EventInfo類型提供的更好用的addEventHandler和removeEventHandler方法。
一下實例應用程序演示了用反射來訪問類型成員的各類方式。SomeType類包含多種成員:一個私有字段(m_someField);一個公共構造器(someType),它獲取一個傳引用的int實參;一個公共方法(tostring);一個公共屬性(someProp);以及一個公共事件(someEvent)。定義好someType類型後,我提供了三個不一樣的方法,他們利用反射來訪問someType的成員。三個方法用不一樣的方式作相同的事情。
1 BindToMemberThenInvokeTheMember方法演示瞭如何綁定到成員並調用它。
2 BindToMemberCreateDelegateToMemberThenInvokeTheMember方法演示瞭如何綁定到一個對象或成員,而後建立一個委託來引用該對象或成員。經過委託來調用的速度很快。若是須要在相同的對象上屢次調用相同的成員,這個技術的性能比上一個好。
3 UseDynamicToBindAndInvokeTheMember方法演示瞭如何利用C#的dynamic基元類型簡化成員訪問語法。此外,在相同類型的不一樣對象上調用相同成員時,這個計數還能提供不錯的性能,由於針對每一個類型,綁定都只會發生一次。並且能夠緩存起來,之後屢次調用的速度會很是快。用這個計數也能夠調用不一樣類型的對象的成員。
internal sealed class SomeType { private int m_someField; public SomeType(int x) { x *= 2; } public override string ToString() { return m_someField.ToString(); } public int SomeProp { get { return m_someField; } set { if (value<1) { throw new ArgumentOutOfRangeException("value"); } m_someField = value; } } public event EventHandler SomeEvent; private void NoCompilerWarnings() { SomeEvent.ToString(); } } class Program { static void Main(string[] args) { Type t = typeof(SomeType); BindToMemberThenInvokeTheMember(t); Console.WriteLine(); BindToMemberCreateDelegateToMemberThenInvokeTheMember(t); Console.WriteLine(); UseDynamicToBindAndInvokeTheMember(t); Console.WriteLine(); } private static void BindToMemberThenInvokeTheMember(Type t) { Console.WriteLine("BindToMemberThenInvokeTheMember"); //構造實例 Type ctorArgument = Type.GetType("System.Int32"); //或者typeof(Int32).MakeByRefType(); IEnumerable<ConstructorInfo> ctors = t.GetTypeInfo().DeclaredConstructors; ConstructorInfo ctor =ctors.First(c => c.GetParameters()[0].ParameterType == ctorArgument); //ConstructorInfo ctor = t.GetTypeInfo().DeclaredConstructors // .First(c => c.GetParameters()[0].ParameterType == ctorArgument); object[] args=new object[]{12};//構造器的實參 Console.WriteLine("x before constructor called:"+args[0]); object obj = ctor.Invoke(args); Console.WriteLine("Type"+obj.GetType()); Console.WriteLine("x after constructor returns"+args[0]); //讀寫字段 FieldInfo fi = obj.GetType().GetTypeInfo().GetDeclaredField("m_someField"); fi.SetValue(obj,33); Console.WriteLine("someField:"+fi.GetValue(obj)); //調用方法 MethodInfo mi = obj.GetType().GetTypeInfo().GetDeclaredMethod("ToString"); string s = (string) mi.Invoke(obj, null); Console.WriteLine("ToString:"+s); //讀寫屬性 PropertyInfo pi = obj.GetType().GetTypeInfo().GetDeclaredProperty("SomeProp"); try { pi.SetValue(obj,0,null); } catch (TargetInvocationException e) { if (e.InnerException.GetType()!=typeof(ArgumentOutOfRangeException)) { throw; } Console.WriteLine("Property set catch "); } pi.SetValue(obj,2,null); Console.WriteLine("SomeProp:"+pi.GetValue(obj,null)); //爲事件添加和刪除委託 EventInfo ei = obj.GetType().GetTypeInfo().GetDeclaredEvent("SomeEvent"); EventHandler eh=new EventHandler(EventCallback); ei.AddEventHandler(obj,eh); ei.RemoveEventHandler(obj,eh); } //添加到事件的回調方法 private static void EventCallback(object sender,EventArgs e){} private static void BindToMemberCreateDelegateToMemberThenInvokeTheMember(Type t) { Console.WriteLine("BindToMemberCreateDelegateToMemberThenInvokeTheMember"); //構造實例()不能建立對構造器的委託 Object[] args=new object[]{12}; Console.WriteLine("x before constructor called:"+args[0]); object obj = Activator.CreateInstance(t,args); Console.WriteLine("Type"+obj.GetType()); Console.WriteLine("x after constructor returns"+args[0]); //注意:不能建立對字段的委託 //調用方法 MethodInfo mi = obj.GetType().GetTypeInfo().GetDeclaredMethod("ToString"); var toString = mi.CreateDelegate<Func<string>>(obj); string s = toString(); Console.WriteLine("ToString:"+s); //讀寫屬性 PropertyInfo pi = obj.GetType().GetTypeInfo().GetDeclaredProperty("SomeProp"); var setSomeProp = pi.SetMethod.CreateDelegate<Action<int>>(obj); try { setSomeProp(0); } catch (ArgumentOutOfRangeException) { Console.WriteLine("Property set catch "); } setSomeProp(2); var getSomeProp=pi.GetMethod.CreateDelegate<Func<int>>(obj); Console.WriteLine("SomeProp:"+getSomeProp()); //爲事件添加和刪除委託 EventInfo ei = obj.GetType().GetTypeInfo().GetDeclaredEvent("SomeEvent"); var addSomeEvent = ei.AddMethod.CreateDelegate<Action<EventHandler>>(obj); addSomeEvent(EventCallback); var removeSomeEvent = ei.RemoveMethod.CreateDelegate<Action<EventHandler>>(obj); removeSomeEvent(EventCallback); } private static void UseDynamicToBindAndInvokeTheMember(Type t) { //構造實例()不能建立對構造器的委託 Object[] args = new object[] { 12 }; Console.WriteLine("x before constructor called:" + args[0]); dynamic obj = Activator.CreateInstance(t, args); Console.WriteLine("Type" + obj.GetType()); Console.WriteLine("x after constructor returns" + args[0]); //讀寫字段 try { obj.m_someField = 5; int v=(int) obj.m_someField; Console.WriteLine("someField:"+v); } catch (RuntimeBinderException e) { Console.WriteLine("failed to access field: "+e.Message); } //調用方法 string s = (string)obj.ToString(); Console.WriteLine("ToString:" + s); //讀寫屬性 try { obj.SomeProp=0; } catch (ArgumentOutOfRangeException e) { Console.WriteLine("Property set catch "); } obj.SomeProp=2; int val =(int)obj.SomeProp; Console.WriteLine("SomeProp:" + val); //爲事件添加和刪除委託 obj.SomeEvent+=new EventHandler(EventCallback); obj.SomeEvent-=new EventHandler(EventCallback); } } internal static class ReflectionExtensions { public static TDelegate CreateDelegate<TDelegate>(this MethodInfo mi,object target=null) { return (TDelegate) (Object) mi.CreateDelegate(typeof(TDelegate), target); } }
運行輸出
BindToMemberThenInvokeTheMember x before constructor called:12 TypeConsoleApp2.SomeType x after constructor returns12 someField:33 ToString:33 Property set catch SomeProp:2 BindToMemberCreateDelegateToMemberThenInvokeTheMember x before constructor called:12 TypeConsoleApp2.SomeType x after constructor returns12 ToString:0 Property set catch SomeProp:2 x before constructor called:12 TypeConsoleApp2.SomeType x after constructor returns12 failed to access field: 「ConsoleApp2.SomeType.m_someField」不可訪問,由於它具備必定的保護級別 ToString:0 Property set catch SomeProp:2 請按任意鍵繼續. . .
許多應用程序都綁定了一組類型(Type對象)或類型成員(MemberInfo派生對象),並將這些對象保存在某種形式的集合中。之後,應用程序搜索這個集合,查找特定對象,而後調用(invoke)這個對象。這個機制很好,只是有個小問題:type和memberinfo派生對象須要大量內存。因此,若是應用程序容納了太多這樣的對象,但只是偶爾調用,應用程序消耗的內存就會急劇增長,對應用程序的性能產生負面影響。
clr內部用更精簡的方式表示這種信息。clr之因此爲應用程序建立這些對象,只是爲了方便開發人員。clr不須要這些大對象就能運行。若是須要保存/緩存大量type和memberinfo派生對象,開發人員可使用句柄(runtime handle)代替對象以減少工做集內存。FCL定義了三個運行時句柄類型(所有都在system命名空間),包括RuntimeTypeHandle,RuntimeFieldHandle和RuntimeMethodHandle。三個類型都是值類型,都只包含一個字段,也就是一個IntPtr;這使類型的實例顯得至關精簡。intPtr字段是一個句柄,引用AppDomain的Loader堆中的一個類型、字段或方法。所以,如今須要以一種簡單、搞笑的方式將重量級的type或memberInfo對象轉換爲輕量級的運行時句柄實例,反之亦然。幸虧,使用如下轉換方法和屬性可輕鬆達到目的。
如下實例程序獲取許多methodInfo對象,把它們轉換爲RuntimeMethodHandle實例,並演示了轉換先後的工做集的差別。
private const BindingFlags c_bf = BindingFlags.FlattenHierarchy | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic; static void Main(string[] args) { //顯示在任何反射操做以前堆的大小 Show("before doing anything"); //爲MSCorlib.dll中全部方法構建MethodInfo對象緩存 List<MethodBase> methodInfos=new List<MethodBase>(); foreach (Type t in typeof(object).Assembly.GetExportedTypes()) { //跳過任何泛型類型 if (t.IsGenericTypeDefinition) { continue; } MethodBase[] mb = t.GetMethods(c_bf); methodInfos.AddRange(mb); } //顯示當綁定全部方法以後,方法的個數和堆的大小 Console.WriteLine("# OF methods={0:N0}",methodInfos.Count); Show("after buiding cache of MethodInfo objects"); //爲全部methodINFO對象構建RuntimeMethodHandle緩存 List<RuntimeMethodHandle> methodHandles = methodInfos.ConvertAll<RuntimeMethodHandle>(c => c.MethodHandle); //GC.KeepAlive(methodInfos);//阻止緩存被過早垃圾回收 Show("Holding MethodInfo And RuntimeMethodHandle CACHE"); //methodInfos = null;//如今容許緩存垃圾回收 //GC.Collect(); methodInfos = methodHandles.ConvertAll<MethodBase>(c => MethodBase.GetMethodFromHandle(c)); Show("size of heap after re_createing methodInfo objects"); methodInfos = null;//如今容許緩存垃圾回收 Show("After freeing methodinfo objects"); GC.KeepAlive(methodHandles);//組織緩存被過早垃圾回收 //GC.KeepAlive(methodInfos);//組織緩存被過早垃圾回收 methodHandles = null; //methodInfos = null;//如今容許緩存垃圾回收 Show("after freeing methodInfos and RuntimeMethodHandles"); Console.ReadKey(); } private static void Show(string s) { Console.WriteLine("Heap size={0,2:N0}-{1}",GC.GetTotalMemory(true),s); }
輸出以下
Heap size=22,440-before doing anything # OF methods=54,346 Heap size=4,102,652-after buiding cache of MethodInfo objects Heap size=4,320,108-Holding MethodInfo And RuntimeMethodHandle CACHE Heap size=4,181,160-size of heap after re_createing methodInfo objects Heap size=3,963,736-After freeing methodinfo objects Heap size=89,824-after freeing methodInfos and RuntimeMethodHandles