在面試中,通常會考察反射的定義(操作元數據),可以用反射做什麼(獲得程序集及其各個部件),反射有什麼使用場景(ORM,序列化,反序列化,值類型比較等)。如果答得好,還可能會問一下如何優化反射(Emit法,委托法)。
反射的性能遠遠低於直接調用,但對於必須要使用的場景,它的性能並非不可接受。對於“反射肯定是造成性能差的主要原因”這種說法,要冷靜客觀的分析。
.NET平台可以使用元數據完整的描述類型(類,結構,委托,枚舉,接口)。許多.NET技術,例如WCF或序列化都需要在運行時發現類型格式。在.NET中,查看和操作元數據的動作稱為反射(也稱為元編程)。
反射就是和程序集打交道。上圖顯示了程序集的階層關系。通過反射我們可以:
使用反射時,一個重要的類型是System.Type類,其會返回加載堆上的類型對象(包括靜態成員和方法表)。當我們要反射一個類的方法時,首先要獲得它的類型對象,然後再使用GetMethods方法獲得某個方法。獲得方法之後,可以使用Invoke執行方法。
反射帶來了非常強大的元編程能力,例如動態生成代碼。如Ruby的元編程能力,它的ORM可以從數據庫的Schema中直接“挖出”字段,而類本身幾乎無需定義任何內容,這就是元編程的威力表現之一。
在很多時候反射是唯一的選擇。
當我們需要動態加載某個程序集(而不是在程序開始時就加載),需要使用反射。但反射最常見的場景是,對象是未知的,或來自外部,或是一個通用的模型例如ORM框架,其針對的對象可以是任何類型。例如:對象的序列化和反序列化。
為什麼我們會選擇使用反射?因為我們沒有辦法在編譯期通過靜態綁定的方式來確定我們要調用的對象。例如一個ORM框架,它要面對的是通用的模型,此時無論是方法也好屬性也罷都是隨應用場景而改變的,這種完全需要動態綁定的場景下自然需要運用反射。還例如插件系統,在完全不知道外部插件究竟是什麼東西的情況下,是一定無法在編譯期確定的,因此只能使用動態加載進行加載,然後通過反射探查其方法,並反射調用方法。
當我們比較兩個引用類型的變量是否相等時,我們比較的是這兩個變量所指向的是不是堆上的同一個實例(內存地址是否相同)。而當我們比較兩個結構體是否相等時,怎麼做呢?因為變量本身包含了結構體所有的字段(數據),所以在比較時,就需要對兩個結構體的字段進行逐個的一對一的比較,看看每個字段的值是否都相等,如果任何一個字段的值不等,就返回false。
實際上,執行這樣的一個比較並不需要我們自己編寫代碼,Microsoft已經為我們提供了實現的方法:所有的值類型繼承自System.ValueType,ValueType和所有的類型都繼承自System.Object,Object提供了一個Equals()方法,用來判斷兩個對象是否相等。但是ValueType覆蓋了Object的Equals()方法。當我們比較兩個值類型變量是否相等時,可以調用繼承自ValueType類型的Equals()方法。這個復寫的方法內部使用了反射,獲得值類型所有的字段,然後進行比較。
先寫一個用於演示的類型:
public class Class1 { public int aPublicField; private int aPrivateField; public int aPublicProperty { get; set; } private int aPrivateProperty { get; set; } public event EventHandler aEvent; //Ctor public Class1() { } public void HelloWorld() { Console.WriteLine("Hello world!"); } public int Add(int a, int b) { return a + b; } }
早期綁定就是傳統的方式:CLR在運行代碼之前,掃描任何可能的類型,然後建立類型對象。晚期綁定則相反,在運行時才建立類型對象。我們可以用System.Reflection中的Assembly類型動態加載程序集。(在需要的時候加載一個外部的程序集)
如果可以選擇早期綁定,那麼當然是早期綁定更好。因為CLR在早期綁定時會檢查類型是否錯誤,而不是在運行時才判斷。
當試圖使用晚期綁定時,你是在引用一個在運行時沒有加載的程序集。你需要先使用Assembly.Load或LoadFrom方法找到程序集,然後你可以使用GetType獲得該程序集的一個類型,最後,使用Activator.CreateInstance(你獲得的類型對象)創建該類型的一個實例。
注意,這樣創建的類型實例是Object類型。(C# 4引入了動態類型之後,也可以用dynamic修飾這種類型的實例)這個類型對象的方法都不可見,如果要使用它的方法,只能使用反射(例如使用GetMethods獲得方法信息,然後再Invoke)。這是反射最普遍的應用場景。
當然,你不應該引用該程序集,否則,就變成早期綁定了。假設我們將上面的演示類型放在一個class library中,然後,在另一個工程中進行晚期綁定。此時我們不將該class library加入參考,而是采用反射的方式,我們試圖獲取演示類,並創建一個實例,就好像我們加入了參考一樣。
class Program { static void Main(string[] args) { Assembly a = null; try { a = Assembly.LoadFile(@"C:\CSharpBasic\ReflectionDemoClass\bin\Debug\ReflectionDemoClass.dll"); } catch (Exception ex) { //Ignore } if (a != null) { CreateUsingLateBinding(a); } Console.ReadLine(); } static void CreateUsingLateBinding(Assembly asm) { try { // 獲得實例類型,ReflectionDemoClass是命名空間的名字 Type t = asm.GetType("ReflectionDemoClass.Class1"); // 晚期綁定建立一個Class1類型的實例 object obj = Activator.CreateInstance(miniVan); // 獲得一個方法 MethodInfo mi = t.GetMethod("HelloWorld"); // 方法的反射執行(沒有參數) mi.Invoke(obj, null); } catch (Exception ex) { Console.WriteLine(ex.Message); } } }
使用動態類型可以簡化晚期綁定。
獲得類型成員需要先持有一個類型。我們通常通過typeof(這是GetType方法的簡寫)獲得類型對象,然後再使用各種方法獲得類型的成員:
GetMembers:默認只獲得公開的成員,包括自己和類型所有父類的公開成員。成員包括字段,屬性,方法,構造函數等。若想獲得特定的成員,可以傳入BindingFlags枚舉,可以傳入多個枚舉值:
BindingFlags枚舉被Flags特性修飾,Flags特性非常適合這種類型的枚舉:每次傳入的成員數是不定的。從定義上可以看到,每個枚舉對應一個數字,其都是2的整數冪:
Default = 0,
IgnoreCase = 1,
DeclaredOnly = 2,
Instance = 4,
Static = 8,
Public = 16,
NonPublic = 32,
……
這種做法有一個特性,就是假如你指定任意一個非負的數字,它都可以唯一的表示成上面各個成員的和,而且只有一種表示方法。例如3可以看成IgnoreCase加上DeclaredOnly,12可以看成Instance加上Static。所以如果你傳入Static + Instance(獲得靜態或者實例成員),實際上你傳入的是數字12,編譯器將你的數字拆成基本成員的和。
至於為什麼只能使用2的整數冪,這是因為2進制中,所有的數字都由0或者1構成。假如我們將上面的列表轉化為2進制:
Default = 00000000,
IgnoreCase = 00000001,
DeclaredOnly = 00000010,
Instance = 00000100,
Static = 00001000,
Public = 00010000,
NonPublic = 00100000,
……
這裡做了八位,實際上位數的長度由最後一個成員確定。那麼對於任意一個非負整數,它的每一位要麼是1要麼是0。我們將1看作開,0看作關,則每個基本成員都相當於打開了一個特定的位,輸入中的每一位如果是1,它就等效於對應的成員處於打開狀態。例如取下面的輸入00011001,它的第4,5和8位是打開的,也就是說,它等於Public + Static +IgnoreCase。這樣我們就可以將它表示為基本成員的相加了。顯而易見,這種相加只有一種方式,不存在第二種方式了。
若想使用Flags特性,你需要自己將值賦予各個成員。值必須是2的整數冪,否則Flags特性將失去意義。
如果只想獲得方法或者屬性,也可以考慮不使用GetMembers+BindingFlags枚舉的方式,直接使用GetMethods或GetProperties方法。以下列出了一些獲得某種特定類型成員的方法:
ConstructorInfo[] GetConstructors()
獲取指定類型包含的所有構造函數
EventInfo[] GetEvents();
獲取指定類型包含的所有事件
FieldInfo[] GetFields();
獲取指定類型包含的所有字段
MemberInfo[] GetMembers();
獲取指定類型包含的所有成員
MethodInfo[] GetMethods();
獲取指定類型包含的所有方法
PropertyInfo[] GetProperties();
獲取指定類型包含的所有屬性
獲得成員之後,我們可以通過相對應的Info類中的成員,來獲得成員的值,類型,以及其他信息。需要注意的是,即使成員是私有或受保護的,通過反射一樣可以獲得其值,甚至可以對其值進行修改。這是ORM的實現基礎。這裡的演示我們就省去晚期綁定,直接將演示類型寫在同一個文件中,例如:
class Program { public static void Main(string[] args) { ReflectionDemoClass r = new ReflectionDemoClass(); //不能在外界訪問私有字段 //r.APrivateField = "1"; var t = typeof(ReflectionDemoClass); FieldInfo[] finfos = t.GetFields(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly); //通過反射獲得私有成員的值 foreach (FieldInfo finfo in finfos) { //甚至修改私有成員的值 if (finfo.Name == "APrivateField") { finfo.SetValue(r, "12345"); } Console.WriteLine("字段名稱:{0}, 字段類型:{1}, 值為:{2}", finfo.Name, finfo.FieldType, finfo.GetValue(r)); } Console.ReadKey(); } } public class ReflectionDemoClass { private string APrivateField; private string AProperty { get; set; } public string AnotherProperty { get; set; } public void AMethod() { Console.WriteLine("I am a method."); } public void AnotherMethod(string s) { Console.WriteLine("I am another method, input is " + s); } public ReflectionDemoClass() { APrivateField = "a"; AProperty = "1"; AnotherProperty = "2"; } }
類型成員除了字段,還有屬性,方法,構造函數等。可以通過Invoke調用方法。
//調用方法 var method = t.GetMethod("AMethod"); //方法沒有輸入變量 method.Invoke(r, null); //方法有輸入變量 method = t.GetMethod("AnotherMethod"); object[] parameters = { "Hello world!" }; method.Invoke(r, parameters);
方法的調用可以分為三種方法:直接調用,委托調用和反射調用。
下面的例子說明了方法的反射調用。假設我們要通過反射更改某個屬性的值,這需要呼叫屬性的setter。
public static void Main(string[] args) { var r = new ReflectionDemoClass(); var t = typeof(ReflectionDemoClass); //獲得屬性的setter var pinfo = t.GetProperty("AnotherProperty"); var setMethod = pinfo.GetSetMethod(); Stopwatch sw = new Stopwatch(); sw.Start(); for (int i = 0; i < 1000000; i++) { setMethod.Invoke(r, new object[] { "12345" }); } sw.Stop(); Console.WriteLine(sw.Elapsed + " (Reflection invoke)"); sw.Restart(); //直接調用setter for (int i = 0; i < 1000000; i++) { r.AnotherProperty = "12345"; } sw.Stop(); Console.WriteLine(sw.Elapsed + " (Directly invoke)"); }
00:00:00.2589952 (Reflection invoke)
00:00:00.0040643 (Directly invoke)
一共調用了一百萬次,從結果來看,反射消耗時間是直接調用的60多倍。
反射速度慢有如下幾個原因:
資料:http://www.cnblogs.com/firelong/archive/2010/06/24/1764597.html
使用反射調用方法比直接調用慢上數十倍。反射優化的根本方法只有一條路:避開反射。然而,避開的方法可分為二種:
1. 用委托和表達式樹去調用。(繞彎子)
2. 生成直接調用代碼,替代反射調用。可以使用System.Reflection.Emit,但如果方法過於復雜,需要非常熟悉IL才可以寫出正確的代碼。
這兩種方法的速度不相上下,擴展閱讀中,有使用委托調用增強反射性能的例子。我們通過表達式樹來創建強類型的委托,達到調用方法的目的(調用方法也是一個表達式)。這可以大大減少耗時,提高性能。
簡單來說,就是你完全可以創造一個動態程序集,有自己的類,方法,屬性,甚至以直接寫IL的方式來做。
精通C#第6版第18章對Emit有詳細的論述。Emit命名空間提供了一種機制,允許在運行時構造出新的類型或程序集。這可以看成是反射的一種類型,但又高於反射(反射只是操作,而Emit可以創造)。
一個常見的Emit的應用場景是Moq,它利用Emit在運行時,動態的創建一個新的類型,實現所有的方法,但都是空方法,從而達到構建一個假的類型的目的。
使用Emit構建新的類型(以及它的屬性和方法)需要對IL有一定認識。因為Emit的大部分方法是直接被轉換為IL的。構建新的類型通常需要以下步驟:
例如,假如我們要構造下面方法的IL代碼(使用Emit):
public void AMethod() { Console.WriteLine("I am a method."); }
下面是示例:
public static MethodInfo EmitDemo() { //創建程序集 AssemblyName name = new AssemblyName { Name = "MyFirstAssembly" }; //獲取當前應用程序域的一個引用 AppDomain appDomain = System.Threading.Thread.GetDomain(); //定義一個AssemblyBuilder變量 //從零開始構造一個新的程序集 AssemblyBuilder abuilder = appDomain.DefineDynamicAssembly(name, AssemblyBuilderAccess.Run); //定義一個模塊(Module) ModuleBuilder mbuilder = abuilder.DefineDynamicModule("MyFirstModule"); //創建一個類(Class) TypeBuilder emitDemoClass = mbuilder.DefineType("EmitDemoClass", TypeAttributes.Public | TypeAttributes.Class); Type ret = typeof(void); //創建方法 MethodBuilder methodBuilder = emitDemoClass.DefineMethod("AMethod", MethodAttributes.Public | MethodAttributes.Static, ret, null); //為方法添加代碼 //假設代碼就是ReflectionDemoClass中AMethod方法的代碼 ILGenerator il = methodBuilder.GetILGenerator(); il.EmitWriteLine("I am a method."); il.Emit(OpCodes.Ret); //在反射中應用 Type emitSumClassType = emitDemoClass.CreateType(); return emitSumClassType.GetMethod("AMethod"); }
從上面的例子可以看到,我們需要和IL打交道,才能在il.Emit中寫出正確的代碼。我們可以通過ildasm查看IL代碼,但如果IL很長,則代碼很難寫對,而且異常非常難以理解。有興趣的同學可以參考:
http://www.cnblogs.com/shinings/archive/2009/02/07/1385760.html 以及 http://sunct.iteye.com/blog/745904
http://www.cnblogs.com/fish-li/archive/2013/02/18/2916253.html 一文中有使用Emit對setter的實現。從結果來看,其速度不如委托快。對於需要大量使用反射的場景,例如ORM需要通過反射為屬性一個一個賦值,那麼它一般也會使用類似的機制來提高性能。
如果需要自己寫一個ORM框架,則為屬性賦值和得到屬性的值肯定是不可避免的操作。我們可以通過Delegate.CreateDelegate建立一個委托,其目標函數是屬性的setter,故它有一個輸入變量,沒有返回值。當Invoke委托時,就調用了setter。編寫代碼時,目標在於構造一個和目標方法簽名相同的委托。
代碼如下:
public static void Main(string[] args) { var r = new ReflectionDemoClass(); var t = typeof(ReflectionDemoClass); //獲得屬性的setter var pinfo = t.GetProperty("AnotherProperty"); var setMethod = pinfo.GetSetMethod(); Stopwatch sw = new Stopwatch(); sw.Start(); for (int i = 0; i < 1000000; i++) { setMethod.Invoke(r, new object[] { "12345" }); } sw.Stop(); Console.WriteLine(sw.Elapsed + " (Reflection invoke)"); sw.Restart(); //直接調用setter for (int i = 0; i < 1000000; i++) { r.AnotherProperty = "12345"; } sw.Stop(); Console.WriteLine(sw.Elapsed + " (Directly invoke)"); //委托調用 //建立一個DelegateSetter類型的委托 //委托的目標函數是ReflectionDemoClass類型中AnotherProperty屬性的setter DelegateSetter ds = (DelegateSetter) Delegate.CreateDelegate(typeof(DelegateSetter), r, //獲得屬性的setter typeof(ReflectionDemoClass).GetProperty("AnotherProperty").GetSetMethod()); sw.Reset(); sw.Start(); for (int i = 0; i < 1000000; i++) { ds("12345"); } sw.Stop(); Console.WriteLine(sw.Elapsed + " (Delegate invoke)"); Console.ReadKey(); }
結果:
00:00:00.3690372 (Reflection invoke)
00:00:00.0068159 (Directly invoke)
00:00:00.0096351 (Delegate invoke)
可以看到委托調用遠遠勝於反射調用,雖然它還是比不上直接調用快速。對於一個通用的解決方案,我們需要定義一個最最一般類型的委托 - Func<object, object[], object>(接受一個object類型與object[]類型的參數,以及返回一個object類型的結果)。
因為任何事物都是表達式,所以當然也可以通過表達式來執行一個委托。雖然使用表達式比較復雜,但我們可以令表達式接受一般類型的委托,避免每次委托調用都要聲明不同的委托。
http://www.cnblogs.com/JeffreyZhao/archive/2008/11/24/invoke-method-by-lambda-expression.html#!comments 該文章使用委托+表達式樹法,給出了一個一般的解決方案。它的結果表明,委托的速度略慢於直接調用,但遠快過反射。
擴展閱讀中,詳細的介紹了委托+表達式樹法對反射的優化。可以使用合適的數據結構進行緩存,從而進一步提高性能。對於使用何種數據結構,擴展閱讀中有詳細的解釋和代碼。這些內容遠遠超過了一般公司(即使是BAT)的面試水平,如果不是有開發需求,不需要對這方面進行深入研究。
http://www.cnblogs.com/JeffreyZhao/archive/2009/10/16/jiri-reflection-argue-1-tech.html
http://www.cnblogs.com/JeffreyZhao/archive/2009/02/01/Fast-Reflection-Library.html
http://www.cnblogs.com/fish-li/archive/2013/02/18/2916253.html