C#筆記 – 反射

反射
  • 元數據表是程序集/模塊中各種成員定義的容器,(System.Reflection)反射則用來解析元數據表

    • 實際上,System.Reflection中的類型為程序集/模塊中包含的元數據提供了一個對象模型

  • 一般使用時機

    • 需要理解類型的定義才能提供豐富功能的類庫

      • 編輯器工具開發

    • 從特定程序集加載特定類型以執行特定任務

      • 晚期綁定:在運行時才確定要使用的類型和方法

  • 性能問題

    • 反射嚴重依賴字符串

      • 編譯時無法保證類型安全

      • 反射的運行,實際上是在運行時,掃瞄程序集的元數據,不停地執行不區分大小寫的字符串搜索

    • 調用方法的打包/解包

      • 調用方法前,CLR會檢查實參具有正確的數據類型和有正確的安全權限

      • 調用方法時,實參需要打包成數組;然後在內部,反射再將這些實參解包到線程棧上

      • 替代方案

        • 讓類型從編譯時已知的基類派生。運行時構造派生類的實例,將對它的引用放到基類的變量中,然後調用基類的虛方法

        • 讓類型實現編譯時已知的接口。運行時構造類型的實例,將對它的引用放到接口類型的變量中,然後調用接口定義的方法

  • 用途

    • 判斷程序集定義了哪些類型/成員

      • Assembly.ExportedTypes

      • System.Reflection.MemberInfo

        • TypeInfo

        • FieldInfo

        • MethodBase

          • ConstructorInfo

          • MethodInfo

        • PropertyInfo

        • EventInfo

        • 層次:

          • 基於AppDomain -> 發現程序集

          • 基於程序集 -> 發現模塊

          • 基於程序集/模塊 -> 發現類型

          • 基於類型 -> 發現

            • 嵌套類型

            • 字段

            • 構造器

            • 方法

            • 屬性

            • 事件

            • 接口

          • 基於各方法(構造/屬性訪問器/事件增刪/一般方法)-> 發現參數/返回值

          • 基於任何一項都可發現定制特性

      • 調用這些成員

        • 成員類型調用成員時需要使用的方法
          FieldInfoGetValue/SetValue
          ConstructorInfoInvoke
          MethodInfoInvoke
          PropertyInfoGetValue/SetValue
          EventInfoAddEventHandler/RemoveEventHandler
      • 運行時句柄

        • RuntimeTypeHandle

          • Type.GetTypeHandle -> RuntimeTypeHandle

          • Type.GetTypeFromHandle -> Type

        • RuntimeFieldHandle

          • FieldInfo.FieldHandle -> RuntimeFieldHandle

          • FieldInfo.GetFieldFromHandle -> FieldInfo

        • RuntimeMethodHandle

          • MethodInfo.MethodHandle -> RuntimeMethodHandle

          • MethodInfo.GetMethodFromHandle -> MethodInfo

        • 值類型,包含了一個IntPtr字段,用於引用AppDomain的Loader堆中的一個類型/字段/方法,目的是為了避免大量Type/MemberInfo派生對象的緩存

    • 得到類型對象的準確含義

      • Object.GetType

        • 判斷指定對象的類型,返回該類型的Type對象引用

        • TestReflection tr1 = new TestReflection();            
          Console.WriteLine(tr1.GetType()); //CLR_Ch23.TestReflection
      • System.Type.GetType

        • 必須傳遞一個字符串參數,指定類型的全名(包括命名空間)

        • 不允許使用編譯器指定的基元類型

        • 方法檢查調用程序集是否定義了指定名稱的類型,如果是,返回一個Type對象的引用

          • 如果調用程序集中沒有定義,剛在MSCorLib.dll中查找

          • Type t = Type.GetType("CLR_Ch23.TestReflection");
            Console.WriteLine(t.Name); //TestReflection
            Console.WriteLine(t.FullName); //CLR_Ch23.TestReflection
        • System.Type.ReflectionOnlyGetType

          • 邏輯與GetType相似,但只以「僅反射」的方式加載,不能執行

      • System.TypeInfo

        • 提供實例成員:DeclaredNestedTypes、GetDeclaredNestedType

        • System.Type對象是輕量級的對象引用,而TypeInfo才代表真實類型定義

        • Type<->TypeInfo

          • 使用System.Reflection.IntrospectionExtensions.GetTypeInfo將Type轉換成TypeInfo

          • TypeInfo.AsType將TypeInfo對象轉換成Type對象

        • 獲取TypeInfo對象會強迫CLR確保已加載類型的定義程序集,從而對類型進行解析,代價高昂

          • TypeInfo包括:

            • 與類型關聯的標志,如:IsSealed, IsAbstract, IsClass, IsPublic, …

            • 定義該類型的程序集的相關信息

            • 類型基類的引用

      • System.Reflection.Assembly

        • 提供實例成員:GetType、DefinedTypes、ExportedTypes

      • typeof操作符

        • 盡量用該操作符獲得type引用

        • 一般用於將晚期綁定的類型信息與早期綁定的類型信息進行比較

          • if(tr1.GetType() == typeof(TestReflection))
            {
               //...
            }
    • 構造類型實例

      • Activator:提供了兩個靜態方法

        • CreateInstance

          • 傳遞Type對象引用

            • Activator.CreateInstance(typeof(TestReflection));
            • 返回新對象的引用

          • 傳遞字符串對象引用

            • 除了Type對象的名稱字符串,還需要傳遞定義了類型程序集的字符串實參

            • 返回的是Runtime.Remoting.ObjectHandle對象

              • 派生自MarshalByRefObject

              • 因此允許將一個AppDomain中創建的對象傳至其他AppDomain

              • 對象不會被實例化,直至調用對象的Unwrap方法

                • 如果對象按引用封送,會創建代理類型和對象

                • 如果對象按值封送,會反序列化對象的副本

              • ObjectHandle handle = Activator.CreateInstance(assemblyName, "TestReflection");
                object tr3 = handle.Unwrap();
        • CreateInstanceFrom

          • 與CreateInstance行為相似,但必須通過字符串來指定類型和程序集,且內部使用LoadFrom

          • 返回ObjectHandle,實例化需要手動調用Unwrap方法

      • AppDomain:提供了四個對象方法,行為與Activator類的方法相似

        • CreateInstance

        • CreateInstanceAndUnwrap

        • CreateInstanceFrom

        • CreateInstanceFromAndUnwrap

        • AppDomain d = AppDomain.CreateDomain("AD1");
          d.CreateInstance("AssemblyName", "TypeName");
          d.CreateInstanceAndUnwrap("AssemblyName", "TypeName");
          d.CreateInstanceFrom("AssemblyName", "TypeName");
          d.CreateInstanceFromAndUnwrap("AssemblyName", "TypeName");
      • 數組實例創建:Array.CreateInstance

        • Array arr = Array.CreateInstance(typeof(TestReflection), 10);
      • 委托實例創建:Delegate.CreateDelegate

        • var Instance = Activator.CreateInstance(typeof(TestReflection));            
          MethodInfo m = typeof(TestReflection).GetMethod("Test");
          Action d = Delegate.CreateDelegate(typeof(Action), Instance, m) as Action;
          d();
      • 泛型類型實例創建:Type.MakeGenericType

        • 首先獲得對開放類型的引用

        • 然後調用Type.MakeGenericType,並向其傳遞類型實參數組

        • 最後調用構建類型實例的相關方法

        • Type openType = typeof(TestGeneric<,,>);
          Type closeType = openType.MakeGenericType(typeof(int), typeof(string), typeof(TestReflection));
          Type[] arg = closeType.GetGenericArguments();
          for (int i = 0; i < arg.Length; i++)
          {
             Console.WriteLine($"Arg{i}: {arg[i]}");
          }
        • 依次輸出:
          Arg0: System.Int32
          Arg1: System.String
          Arg2: CLR_Ch23.TestReflection
        • 然而,目前closeType必須通過GetTypeInfo才能得到具體的封閉類型,否則只能得到System.RuntimeType的結果

          • Console.WriteLine(closeType.GetType()); //System.RuntimeType
            Console.WriteLine(closeType.GetTypeInfo()); //CLR_Ch23.TestGeneric`3[System.Int32, System.String, CLR_Ch23.TestReflection]

參考書目

  • 《CLR via C#》(第4版) Jeffrey Richter