← 筆記

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