.NET面試題系列[6] - 反射

反射 - 定義,實例與優化

在面試中,一般會考察反射的定義(操做元數據),能夠用反射作什麼(得到程序集及其各個部件),反射有什麼使用場景(ORM,序列化,反序列化,值類型比較等)。若是答得好,還可能會問一下如何優化反射(Emit法,委託法)。html

反射的性能遠遠低於直接調用,但對於必需要使用的場景,它的性能並不是不可接受。對於「反射確定是形成性能差的主要緣由」這種說法,要冷靜客觀的分析。面試

.NET平臺可使用元數據完整的描述類型(類,結構,委託,枚舉,接口)。許多.NET技術,例如WCF或序列化都須要在運行時發現類型格式。.NET中,查看和操做元數據的動做稱爲反射(也稱爲元編程)。數據庫

 

 

反射就是和程序集打交道。上圖顯示了程序集的階層關係。經過反射咱們能夠:express

  1. 得到一個程序集:這稱爲動態加載程序集,或者晚期綁定。相對的,早期綁定就是引用程序集,從而在運行時以前就加載它。得到程序集以後,就能夠進一步得到其中的類型,而後再進一步得到類型中的方法,屬性的值等等。
  2. 得到程序集的託管模塊。一個程序集能夠包含多個託管模塊。一般咱們對程序集和類型的名字很熟悉,對模塊則一無所知,因此這一般沒什麼用,由於咱們得到模塊的最終目的仍是爲了模塊中的類型。
  3. 得到程序集中(託管模塊中的)的類型。此時System.Type類起着十分關鍵的做用。它能夠返回類型對象,以後,咱們就能夠得到類型的成員和方法表。得到類型對象以後,咱們就能夠進一步得到類型的成員。
  4. 得到類型的成員。常見的情境有遍歷全部屬性並打印其值,反射調用方法等。ORM經過反射得到類型及其成員,而後爲其賦值。

使用反射時,一個重要的類型是System.Type類,其會返回加載堆上的類型對象(包括靜態成員和方法表)。當咱們要反射一個類的方法時,首先要得到它的類型對象,而後再使用GetMethods方法得到某個方法。得到方法以後,可使用Invoke執行方法。編程

反射帶來了很是強大的元編程能力,例如動態生成代碼。如Ruby的元編程能力,它的ORM能夠從數據庫的Schema中直接「挖出」字段,而類自己幾乎無需定義任何內容,這就是元編程的威力表現之一。緩存

反射有什麼應用場景?

在不少時候反射是惟一的選擇數據結構

當咱們須要動態加載某個程序集(而不是在程序開始時就加載),須要使用反射。但反射最多見的場景是,對象是未知的,或來自外部,或是一個通用的模型例如ORM框架,其針對的對象能夠是任何類型。例如:對象的序列化和反序列化。app

爲何咱們會選擇使用反射?由於咱們沒有辦法在編譯期經過靜態綁定的方式來肯定咱們要調用的對象。例如一個ORM框架,它要面對的是通用的模型,此時不管是方法也好屬性也罷都是隨應用場景而改變的,這種徹底須要動態綁定的場景下天然須要運用反射。還例如插件系統,在徹底不知道外部插件到底是什麼東西的狀況下,是必定沒法在編譯期肯定的,所以只能使用動態加載進行加載,而後經過反射探查其方法,並反射調用方法。 框架

.NET中的反射一例

當咱們比較兩個引用類型的變量是否相等時,咱們比較的是這兩個變量所指向的是否是堆上的同一個實例(內存地址是否相同)。而當咱們比較兩個結構體是否相等時,怎麼作呢?由於變量自己包含告終構體全部的字段(數據),因此在比較時,就須要對兩個結構體的字段進行逐個的一對一的比較,看看每一個字段的值是否都相等,若是任何一個字段的值不等,就返回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枚舉,能夠傳入多個枚舉值:

  • Static:靜態成員
  • Instance:實例成員
  • Public:公開成員
  • NonPublic:非公開成員
  • DeclaredOnly:只返回本身類型內的成員,不返回父類的成員

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多倍。

方法反射調用爲何慢

反射速度慢有以下幾個緣由:

  • 反射首先要操做和查找元數據,而直接調用查找元數據這一步是在編譯(jit)時
  • 反射調用方法時,沒有通過編譯器jit的優化。而直接調用的代碼是經jit優化後的本地代碼
  • 反射調用方法須要檢查輸入參數的類型,這是在運行時才能作到的,而直接調用的代碼檢查類型是在編譯時
  • 儘管能夠經過後面所講的幾種方法優化反射,反射的性能仍然遠遠不如直接調用

資料:http://www.cnblogs.com/firelong/archive/2010/06/24/1764597.html

使用反射調用方法比直接調用慢上數十倍。反射優化的根本方法只有一條路:避開反射。然而,避開的方法可分爲二種:

1. 用委託和表達式樹去調用。(繞彎子)

2. 生成直接調用代碼,替代反射調用。可使用System.Reflection.Emit,但若是方法過於複雜,須要很是熟悉IL才能夠寫出正確的代碼。

這兩種方法的速度不相上下,擴展閱讀中,有使用委託調用加強反射性能的例子。咱們經過表達式樹來建立強類型的委託,達到調用方法的目的(調用方法也是一個表達式)。這能夠大大減小耗時,提升性能。

解決方案1:System.Reflection.Emit

簡單來講,就是你徹底能夠創造一個動態程序集,有本身的類,方法,屬性,甚至以直接寫IL的方式來作。

精通C#第6版第18章對Emit有詳細的論述。Emit命名空間提供了一種機制,容許在運行時構造出新的類型或程序集。這能夠當作是反射的一種類型,但又高於反射(反射只是操做,而Emit能夠創造)。

一個常見的Emit的應用場景是Moq,它利用Emit在運行時,動態的建立一個新的類型,實現全部的方法,但都是空方法,從而達到構建一個假的類型的目的。

使用Emit構建新的類型(以及它的屬性和方法)須要對IL有必定認識。由於Emit的大部分方法是直接被轉換爲IL的。構建新的類型一般須要如下步驟:

  1. 創建一個類,並實現一些類型和方法
  2. 在主函數所在的類型中,定義一個靜態方法,並傳入一個應用程序域
  3. 在應用程序域中建立一個新的程序集
  4. 在程序集中建立一個新的模塊
  5. 在模塊中建立咱們創建的類
  6. 使用ILGenerator建立類型的全部方法(包括構造函數)和屬性,一般,手寫是不太現實的,咱們須要使用ildasm.exe得到IL代碼,而後再使用ILGenerator造出這些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須要經過反射爲屬性一個一個賦值,那麼它通常也會使用相似的機制來提升性能。

解決方案2:委託

若是須要本身寫一個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

相關文章
相關標籤/搜索