反射,一個很有用且有意思的特性。當動態創建某個類型的實例或是調用方法或是訪問對象成員時通常會用到它,它是基於程序集及元數據而工作的,所以這一章我們來討論一下程序集、反射如何工作、如何動態創建類型及對象等相關知識,甚至可以動態創建程序集。
通過本系列的前面章節,我們已經知道,Windows為每個進程分配獨立的內存空間地址,各個進程之間不能直接相互訪問。Windows對.NET的支持是以宿主和COM的形式實現的,基於.NET平台語言實現的代碼文件使用Windows PE的文件格式,CLR其實就是COM,相當於一個虛擬機(當然這個虛擬機可以部署到任意支持它的系統環境中),在安裝.NET Framework時,CLR的組件與其他COM一樣在Windows系統中享有同等的待遇,當CLR啟動初始化時會創建一個應用程序域,應用程序域是一組程序集的邏輯容器,它會隨着進程的終止而被卸載銷毀,CLR把程序代碼所需要的程序集加載到當前(或指定的)應用程序域內。CLR可以以其初始化時創建的應用程序域為基礎再創建其他的新應用程序域,兩個應用程序域中的代碼不能直接訪問,當然可以通過“中介”進行數據傳送。新的程序域創建完后CLR完全可以卸載它,以同步方式調用AppDomain.Unload方法即可,調用此方法后,CLR會掛起當前進程中的所有線程,接着查找並中止運行在即將卸載的程序域內的線程,然后進行垃圾回收,最后主線程恢復運行。
任何Windows程序都可以寄宿CLR,一台機上可以安裝多個版本的CLR。Windows在啟動一個托管的程序時會先啟動MSCorEE.dll中的一個方法,該方法在內部根據一個托管的可執行文件信息來加載相應版本的CLR,CLR初始完成之后,將程序集加載到應用程序域,最后CLR檢查程序集的CLR頭信息找到Main方法並執行它。
程序集是所有類型的集合,它還有一個重要的東西就是元數據。JIT就是利用程序集的TypeRef和AssemblyRef等元數據來確定所引用的程序集及類型,這些元數據包括名稱、版本、語言文化和公鑰標記等,JIT就是根據這些信息來加載一個程序集到應用程序域中。如果要自己加載一個程序集,可以調用類型Assembly的LoadXXX系列方法。
(1) Load重載系列
該方法會按照一定的順序查找指定目錄中的程序集:先去GAC中查找(如果是一個強命名程序集),如果找不到,則去應用程序的基目錄、子目錄查找。如果都沒找到,則拋出異常。如下代碼加載程序集MyAssemblyB:
string assemblyName = "MyAssemblyB, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"; Assembly assembly = Assembly.Load(assemblyName);
(2) LoadFrom重載系列
加載指定程序集名稱或路徑的程序集,其在內部調用Load方法,並且還可以指定一個網絡路徑,如果指定網絡路徑,則先下載該程序集,再將其加載到程序域,如下代碼:
Assembly.LoadFrom("http://solan.cnblogs.com/MyAssembly.dll");
(3) LoadFile重載系列
從任意路徑加載一個程序集,並且可以從不同路徑加載相同名稱的程序集。
在一個項目中,可能程序集之間都有依賴關系,也可以將一個程序集作為資源數據嵌入到一個程序集中,在需要時再加載該程序集,這時通過注冊ResolveAssembly事件來加載這個程序集。如下;
AppDomain.CurrentDomain.AssemblyResolve += (sender, arg) => { byte[] buffer = null; using (Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("ConsoleApp.MyAssemblyA.dll")) { buffer = new byte[stream.Length]; stream.Read(buffer, 0, buffer.Length); } return Assembly.Load(buffer); };
以上代碼要求必須先將MyAssemblyA.dll文件以資源形式嵌入到ConsoleApp項目中。這樣在運行ConsoleApp程序時,如果使用了MyAssemblyA中的類型且未找到MyAssemblyA.dll文件,則會進入上面的事件方法來加載程序集MyAssemblyA。
如果只是想了解一個程序集的元數據分析其類型而不調用類型的成員,為了提高性能,可以調用這些方法:
Assembly.ReflectionOnlyLoadFrom(String assemblyFile) Assembly.ReflectionOnlyLoad(byte[] rawAssembly) Assembly.ReflectionOnlyLoad(String assemblyName)
如果試圖調用上面這三個方法加載的程序集中類型的代碼,則CLR會拋出異常。
我們知道,在程序集(或模塊)內有一個很重要的數據就是元數據,它們描述了類型定義表,字段定義表,方法表等,也就是說所有的類型及成員定義項都會在這里被清楚詳細地記錄下來。很明顯,如果我們拿到了這些“描述信息”,當然就相當於已經明確知道了一個類型及其成員,進而就可以“構造”這個類型,通過反射就可以達到這樣的目的。另人高興的是我們不用分析那些元數據就可以方便地得到程序集內的類型成員,.NET Framework提供了一些與此相關的類定義在命名空間System.Reflection下。
反射提供了封裝程序集、模塊和類型的對象(Type 類型)。反射機制運行在程序運行時動態發現類型及其成員。
(1)查找程序集內所定義的類型
在將某一程序集加載到應用程序域后,可以通過Assembly的GetExportedTypes方法來獲取該程序集所有的公開類型,如下代碼:
private void GetTypes() { string assemblyName = "MyAssemblyB, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"; Assembly assembly = Assembly.Load(assemblyName); Type[] types = assembly.GetExportedTypes(); foreach (Type t in types) { Console.WriteLine(t.Name); } }
(2)查找類型成員
在命名空間System.Reflection中有一個抽象類型MemberInfo,它封裝了與類型成員相關的通用屬性,每一個類型成員都有一個對應的從MemberInfo派生而來的類型,並且內置了一些特殊的屬性特征,如FieldInfo、MethodBase(ContructorInfo、MethodInfo)、PropertyInfo和EventInfo。可以通過調用類型Type對象的GetMembers方法獲取該類型的所有成員或相應成員,如下代碼(對上面的GetTypes方法的修改)獲取全部成員列表:
Type[] types = assembly.GetExportedTypes(); foreach (Type t in types) { Console.WriteLine(t.Name); MemberInfo[] members = t.GetMembers(); }
Type有一組GetXXX方法是獲取對象成員的,以下列出部分方法:
GetConstructor/GetConstructors //獲取構造函數 GetEvent/GetEvents //獲取事件 GetField/GetFields //獲取字段 GetMethod/GetMethods //獲取方法 GetProperty/GetProperties //獲取屬性
並且每個方法都可以接收一個枚舉類型BindingFlags的參數指定控制綁定和由反射執行的成員和類型搜索方法的標志。有關BindingFlags 枚舉可參考MSDN文檔
如下代碼獲取AudiCar類型的Owner屬性和Run()方法:
private void GetTypeMethod() { string assemblyName = "MyAssemblyB, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"; Assembly assembly = Assembly.Load(assemblyName); Type t = assembly.GetType("MyAssemblyB.AudiCar"); MethodInfo method = t.GetMethod("Run"); PropertyInfo pro = t.GetProperty("Owner"); }
(3)構造類型實例
在拿到類型及成員信息之后,我們就可以構造類型的實例對象了。FCL提供了幾個方法來構造一個類型的實例對象,有關這些方法詳細內容,可參考MSDN文檔:
Activator.CreateInstance() //重載系列 Activator.CreateInstanceFrom() //重載系列 AppDomain.CurrentDomain.CreateInstance() //重載系列 AppDomain.CurrentDomain.CreateInstanceFrom() //重載系列
如下構造AudiCar類型的實例:
private void TestCreateInstance() { string assemblyName = "MyAssemblyB, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"; Assembly assembly = Assembly.Load(assemblyName); Type t = assembly.GetType("MyAssemblyB.AudiCar"); var obj = Activator.CreateInstance(t); Debug.Assert(obj != null); }
看一下調試:

另外,還可以調用類型的構造函數創建實例對象,如下:
obj = t.InvokeMember("AudiCar", BindingFlags.CreateInstance, null, null, null);
如果僅僅得到類型的對象,好像意義並不大,我們更多的是要操作對象,比如訪問屬性,調用方法等,這一節我們來看一下如何訪問成員。
類型Type提供了一個訪問目標類型成員的非常靠譜的方法InvokeMember,調用此方法時,它會在類型成員中找到目標成員(這通常指定成員名稱,也可以指定搜索篩選條件BindingFlags,如果調用的目標成員是方法,還可以給方法傳遞參數。),如果找到則調用目標方法,並返回目標訪問返回的結果,如果未找到,則拋出異常,如果是在目標方法內部有異常,則InvokeMember會先捕獲該異常,包裝后再拋出新的異常TargetInvocationException。以下是InvokeMember方法的原型:
public object InvokeMember(string name, BindingFlags invokeAttr, Binder binder, object target, object[] args); public object InvokeMember(string name, BindingFlags invokeAttr, Binder binder, object target, object[] args, CultureInfo culture); public abstract object InvokeMember(string name, BindingFlags invokeAttr, Binder binder, object target, object[] args, ParameterModifier[] modifiers, CultureInfo culture, string[] namedParameters); name 目標方法名稱 invokeAttr 查找成員篩選器 binder 規定了匹配成員和實參的規則 target 要調用其成員的對象 args 傳遞給目標方法的參數
在上一節的最后我們展示了如何調用類型的構造函數來實例化一個對象,下面的代碼演示了如何調用對象的方法,其中方法Turn接收一個Direction類型的參數:
string assemblyName = "MyAssemblyB, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"; Assembly assembly = Assembly.Load(assemblyName); Type t = assembly.GetType("MyAssemblyB.AudiCar"); var obj = Activator.CreateInstance(t); t.InvokeMember("Turn", BindingFlags.InvokeMethod, null, obj, new object[] { Direction.East });
另外,調用目標對象的方法,還可以以MethodInfo的方式進行,如下:
Type t = assembly.GetType("MyAssemblyB.AudiCar"); var obj = Activator.CreateInstance(t); MethodInfo method = t.GetMethod("Turn"); method.Invoke(obj, new object[] { Direction.Weast });
以下是對屬性的讀寫操作:
Type t = assembly.GetType("MyAssemblyB.AudiCar"); var obj = Activator.CreateInstance(t); //為屬性Owner賦值 obj.GetType().GetProperty("Owner").SetValue(obj, "張三", null); //讀取屬性Owner的值 string name = (string)obj.GetType().GetProperty("Owner").GetValue(obj, null);
對於其他成員(如字段等)的訪問,可參考MSDN文檔。
反射對泛型的支持
以上的演示都是針對普通類型,其實反射也提供了對泛型的支持,這里只簡單演示一下反射對泛型的簡單操作。比如我們有如下一個泛型類型定義:
namespace MyAssemblyB { public class MyGeneric<T> { public string GetName<T>(T name) { return "Generic Name:" + name.ToString(); } } }
這個類型很簡單,類型MyGeneric內有一個方法,該方法返回帶有附加信息” Generic Name:”的名稱。先來看一下如何獲取指定參數類型為string的泛型類:
private void TestGenericType() { string assemblyName = "MyAssemblyB, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"; Assembly assembly = Assembly.Load(assemblyName); Type[] types = assembly.GetExportedTypes(); foreach (Type t in types) { //檢測是否泛型(在程序集MyAssemblyB中只定義了一個泛型類型 MyGeneric<T>) if (t.IsGenericType) { //為泛型類型參數指定System.String類型,並創建實例 object obj = Activator.CreateInstance(t.MakeGenericType(new Type[] { typeof(System.String) })); //生成泛型方法 MethodInfo m = obj.GetType().GetMethod("GetName").MakeGenericMethod(new Type[] { typeof(System.String) }); //調用泛型方法 var value = m.Invoke(obj, new object[] { "a" }); Console.WriteLine(value); } } }
調試起來,看一下最終的value值:

反射泛型的時候,要先確定目標類型是泛型,在創建泛型類型實例前,必須調用MakeGenericType方法構造一個真正的泛型,該方法接收一個要指定泛型類型參數的類型數組,同樣調用泛型方法前要調用方法MakeGenericMethod構造相應的泛型方法,此方法也接收一個指定泛型類型的類型數組。
前面幾節所描述的都是基於已經存在程序集的情況下進行反射,.NET Framework還提供了在內存中動態創建類型的強大功能。我們知道程序集包括模塊,模塊包括類型,類型包括成員,在動態創建類型的時候也是要遵循這個順序。動態創建類型是基於元數據的實現方式來實現的,這一部分被定義在命名空間System.Reflection.Emit內,有一系列的XXXBuilder構造器來創建相應的類型對象。我們來看一要動態創建類型,有哪些步驟(這里只是簡單演示):
(1) 程序集是老窩,所以要先創建一個程序集:
AssemblyBuilder aBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(new AssemblyName("TempDynamicAssembly"), AssemblyBuilderAccess.Run);
(2) 有了程序集,接下來是模塊
ModuleBuilder mBuilder = aBuilder.DefineDynamicModule("NotifyPropertyChangedObject");
(3) 接下來就是創建類型了:
this.tBuilder = mBuilder.DefineType(typeFullName, TypeAttributes.Public | TypeAttributes.BeforeFieldInit);
(4) 現在可以創建類型的成員了,為類型創建一個屬性Name。我們知道屬性包含字段和對字段的兩個訪問器,所以應該先創建字段,然后再創建兩個訪問器方法,這一段是按照IL碼的先后順序來的,如下:
FieldBuilder fieldBuilder = this.tBuilder.DefineField(string.Format("{0}Field", propertyName), propertyType, FieldAttributes.Private); PropertyBuilder propertyBuilder = tBuilder.DefineProperty(propertyName, PropertyAttributes.HasDefault, propertyType, null); MethodAttributes getSetAttr = MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig; MethodBuilder getAccessor = tBuilder.DefineMethod(string.Format("get_{0}", propertyName), getSetAttr, propertyType, Type.EmptyTypes); ILGenerator getIL = getAccessor.GetILGenerator(); getIL.Emit(OpCodes.Ldarg_0); getIL.Emit(OpCodes.Ldfld, fieldBuilder); getIL.Emit(OpCodes.Ret); propertyBuilder.SetGetMethod(getAccessor); MethodBuilder setAccessor = tBuilder.DefineMethod(string.Format("set_{0}", propertyName), getSetAttr, null, new Type[] { propertyType }); setAccessor.DefineParameter(1, ParameterAttributes.None, "value"); ILGenerator setIL = setAccessor.GetILGenerator(); setIL.Emit(OpCodes.Nop); setIL.Emit(OpCodes.Ldarg_0); setIL.Emit(OpCodes.Ldarg_1); setIL.Emit(OpCodes.Stfld, fieldBuilder); setIL.Emit(OpCodes.Ldarg_0); setIL.Emit(OpCodes.Ldstr, propertyName); setIL.Emit(OpCodes.Call, this.mBuilder); setIL.Emit(OpCodes.Nop); setIL.Emit(OpCodes.Ret); propertyBuilder.SetSetMethod(setAccessor);
注意,這里面有對事件的操作,可以忽略。
(5) 最后調用類型構造器的CreateType()方法就可以創建該類型了:
tBuilder.CreateType();
該方法返回一個Type類型。
類型創建完成后,我們就可以使用上一節講的反射相關知識對該類型進行操作了,這里當然是一個簡單的類型,如果想創建復雜的類型,比如有方法,事件等成員,那可以發揮你的匯編能力來慢慢折騰吧,也可以體味一下當時匯編程序員們的苦逼!托管下的匯編編碼已經很簡化了,圍繞Emit方法折騰死!如果想研究IL,可以用IL DASM打開托管程序集,慢慢欣賞吧。
在我們的日常開發中,有時用了動態類型還是很方便的,比如當你要創建一個DataGrid的數據源DataTable,但多少列不確定,列的數據類型不確定,列名也不確定的情況下,這時根據要求創建一個動態類型,繼而再創建一個該類型的集合就很方便使用了。我封裝了一個動態創建類型的類,在本文的結尾提供下載,喜歡的可以拿去。
這里所描述的是動態地在內存創建一個類,關於動態類型dynamic和var,這里就不再瞎掰了,感興趣的可以去查找相關資料。
反射為我們開發提供了非常便利的編程實踐,但使用它也有幾點需要注意。
既然是反射,我們在編碼時對類型是未知的,如果是已知,就沒必要再用反射了, 除非是要做類似分析類型元數據的工具,而我們一般使用反射是要操作其屬性字段、調用其方法等,目的是用而不是分析。在編譯使用了反射的代碼過程中,反射的目標類型是不安全的,很有可能在調用反射出來的類對象時出錯,這一點要注意。
反射是基於元數據實現的,所以在使用反射過程中,代碼會搜索程序集的元數據,這些元數據是基於字符串的,並且無法預編譯,所以這一系列的操作對性能有嚴重影響。另外,由於我們對目標類型未知,在向方法傳遞參數時通常是以object數組傳遞,CLR會逐個檢查參數的數據類型,無論是傳入還是返回,都有可能進行大量的類型轉換,這也損傷了性能。所以對於反射的應用,應該注意。當然,像一些ORM等框架是以犧牲性能來換取方便的開發體驗就另當別說了。
最后我們來演示一個簡單的支持插件的小項目也可以彌補上面幾節中丟失的代碼塊。總共有三個項目,模擬對車進行跑和轉彎測試,如圖:

ConsoleApp項目是測試程序
MyAssemblyA 是整個插件系列的接口契約,定義了汽車接口的跑動作Run和轉變動作Turn,我們約定所有的其他插件車必須符合這個契約,即實現這個接口。
接口如下:
namespace MyAssemblyA { public interface ICar { void Run(); void Turn(Direction direction); } } namespace MyAssemblyA { public enum Direction { East, Weast, South, North } }
MyAssemblyB 插件程序,任何一個類型的汽車必須實現MyAssemblyA里的接口約定,這里只是演示,當然可以將每一個類型的汽車分配到一個程序集中去。代碼如下:
namespace MyAssemblyB { //奧迪 public class AudiCar : ICar { public AudiCar() { } public string Owner { get; set; } public void Run() { Console.WriteLine("AudiCar Run"); } public void Turn(Direction direction) { Console.WriteLine("AudiCar Turn: " + direction.ToString()); } } } namespace MyAssemblyB { // 奔馳 public class BenzCar:ICar { public BenzCar() { } public string Owner { get; set; } public void Run() { Console.WriteLine("BenzCar Run"); } public void Turn(Direction direction) { Console.WriteLine("BenzCar Turn: " + direction.ToString()); } } }
在主程序ConsoleApp中,我們只關心車能跑,能轉彎即可,至於它是奧迪還是奔馳,我們不管;至於這車是噴氣還是長翅膀驅動,是靠太陽吸引力轉彎還是人推着轉彎,我們不關心,我們只接受客戶發來一個車的類型名(也可以從配置文件中讀取),來看一下代碼:
public void TestCar(string carType) { //從配置文件中讀取程序集信息 string assemblyName = "MyAssemblyB, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"; Assembly assembly = Assembly.Load(assemblyName); //也可以從配置文件中讀取車的類型 "MyAssemblyB.AudiCar" Type t = assembly.GetType(carType); //構造一輛車 var obj = Activator.CreateInstance(t); ICar car = obj as ICar; //如果這個車符合契約,則進行測試 if (car != null) { car.Run(); car.Turn(Direction.East); } }
這樣我們的主測試程序就不再關心什么車型了,只要符合我們測試的契約即可,降低耦合,提高靈活。
