• 程序集加载和反射


    前言


      本篇讨论程序集的加载及反射。主要涉及到System.Reflection.Assembly和System.Type两个类,前者可以用于访问指定程序集的相关信息,或把程序集加载到程序当中,后者可以访问任何数据类型的信息。以下,是本篇文章涉及的主要内容。


    ##程序集加载

      本节首先介绍Assembly类,该类位于System.Reflection命名空间下,它允许访问指定程序集的元数据,也包含加载和执行程序集的中的方法。下面将介绍几种常用的动态加载程序集的方式:

    方法名称 说明
    Load 加载程序集
    LoadFrom 加载指定路径的程序集
    LoadFile 仅加载指定路径的程序集(不包括依赖项)
    ReflectionOnlyLoad 加载程序集(不执行任何带代码)
    ReflectionOnlyLoadFrom 加载指定路径的程序集(不执行任何代码)

    Assembly.Load

      Assembly的Load方法有几个重载版本,两种最常用的重载:Load(AssemblyName)和Load(String),传入的参数是需要加载的程序集的名称。

    创建控制台应用程序AssemblyAndReflection,向解决方案添加新项目AssemblyLoad,添加类ClassA、ClassB、ClassC,编译后为AssemblyLoad.dll分配强名称并注册到GAC中。

    	public class ClassA
    	{
        	public void SayHello()
        	{
            	Console.WriteLine("Hello.This is ClassA");
        	}
    	}
    
        public class ClassB
        {
            public void SayHello()
            {
                Console.WriteLine("Hello.This is ClassB");
            }
        }
    
        public class ClassC
        {
            public void SayHello()
            {
                Console.WriteLine("Hello.This is ClassC");
            }
        }
    

    向Program.cs中添加如下代码:

    	static void Main(string[] args)
        {
            string fullName = "AssemblyLoad,Version=1.0.0.0,Culture=neutral,PublicKeyToken=098608575f7409cd, processor architecture=MSIL";
    		//string fullName = "AssemblyLoad";
            //Assembly assembly = Assembly.Load(new AssemblyName(fullName));
            Assembly assembly = Assembly.Load(fullName);
    
            if (assembly != null)
            {
                foreach (var c in assembly.GetTypes())
                {
                    Console.WriteLine(c.FullName);
                }
            }
            Console.ReadLine();
            //****************************************************OutPut****************************************************
            //AssemblyLoad.ClassA
            //AssemblyLoad.ClassB
            //AssemblyLoad.ClassC
            //**************************************************************************************************************
        }
    

    注意,Load方法的参数可以是强命名程序集或弱命名程序集(上述代码中注释掉的fullName变量)。传入不同参数时,查找程序集的方式略有不同。

    Assembly.LoadFrom

      Assembly的LoadFrom方法加载指定了路径名的程序集。将AssemblyLoad.dll文件放至D:DLL下,修改上述代码:

    	static void Main(string[] args)
        {
            Assembly assembly = Assembly.LoadFrom(@"D:DLLAssemblyLoad.dll");
    
            if (assembly != null)
            {
                foreach (var c in assembly.GetTypes())
                {
                    Console.WriteLine(c.FullName);
                }
    
            }
    
            Console.ReadLine();
    
            //****************************************************OutPut****************************************************
            //AssemblyLoad.ClassA
            //AssemblyLoad.ClassB
            //AssemblyLoad.ClassC
            //**************************************************************************************************************
        }
    

    LoadFrom的执行原理:

    1. 调用System.Reflection.AssemblyName类的静态方法GetAssemblyName方法,打开指定路径下的文件,返回AssemblyName对象。下面是GetAssemblyName方法的源码:

       [System.Security.SecuritySafeCritical]  // auto-generated
       [ResourceExposure(ResourceScope.None)]
       [ResourceConsumption(ResourceScope.Machine, ResourceScope.Machine)]
       static public AssemblyName GetAssemblyName(String assemblyFile)
       {
           if(assemblyFile == null)
               throw new ArgumentNullException("assemblyFile");
           Contract.EndContractBlock();
      
           // Assembly.GetNameInternal() will not demand path discovery 
           //  permission, so do that first.
           String fullPath = Path.GetFullPathInternal(assemblyFile);
           new FileIOPermission( FileIOPermissionAccess.PathDiscovery, fullPath ).Demand();
           return nGetFileInformation(fullPath);
       }
      
    2. 调用Assembly.Load方法,将步骤1中返回的AssemblyName对象作为参数传入。

    Assembly.LoadFile

      加载指定路径上的程序集文件的内容。LoadFile方法不会加载目标程序集引用和依赖的其他程序集。

    Assembly.ReflectionOnlyLoad和Assembly.ReflectionOnlyLoadFrom

      如果只希望通过反射来分析程序集的元数据,并确保程序集中的任何代码都不会被执行,这种情况下可以使用Assembly类的ReflectionOnlyLoadFrom方法或ReflectionOnlyLoad方法。


    ##获取类型的信息

      本节介绍System.Type类,通过这个类可以访问任何数据类型的信息,System.Type类型是执行类型和对象操作的起点。获取Type对象的几种方式:

    Object.GetType()

    	int x = 100;
        Type t = x.GetType();
    	Console.WriteLine(t.FullName);
    

    System.Type类提供的静态方法ReflectionOnlyGetType()

    	string typeName = Type.ReflectionOnlyGetType("AssemblyLoad.ClassA, AssemblyLoad, Version=1.0.0.0, Culture=neutral, PublicKeyToken=098608575f7409cd, processor architecture=MSIL", false, true).FullName;
        Console.WriteLine(typeName);
    

    System.Reflection.Assembly类提供的实例成员GetTypes、DefinedTypes和ExportedTypes

    	string fullName = "AssemblyLoad,Version=1.0.0.0,Culture=neutral,PublicKeyToken=098608575f7409cd, processor architecture=MSIL";
    
        Assembly assembly = Assembly.Load(fullName);
        Console.WriteLine("assembly.GetTypes():");
        foreach (var t in assembly.GetTypes())
        {
            Console.WriteLine(t.FullName);
        }
        Console.WriteLine();
        Console.WriteLine("assembly.ExportedTypes:");
        foreach (var t in assembly.ExportedTypes)
        {
            Console.WriteLine(t.FullName);
        }
        Console.WriteLine();
        Console.WriteLine("assembly.DefinedTypes:");
        foreach (var t in assembly.DefinedTypes)
        {
            Console.WriteLine(t.FullName);
        }
        Console.WriteLine();
    

    typeof关键字(应尽量使用这个操作符来获取Type引用,因为操作符生成的代码通常更快)

    	Console.WriteLine(typeof(int).FullName);
    

    ##构造类型的实例

      获取对Type派生对象的引用之后,就可以构造该类型的实例了。

    System.Activator.CreateInstance

    string fullName = "AssemblyLoad,Version=1.0.0.0,Culture=neutral,PublicKeyToken=098608575f7409cd, processor architecture=MSIL";
    Type t = Assembly.Load(fullName).GetType("AssemblyLoad.ClassA");
    var o = Activator.CreateInstance(t);
    Console.WriteLine(o.GetType());
    

    ##设计支持加载项的应用程序

    反射的性能

      反射是相当强大的机制,允许在运行时发现并使用编译时还不太了解的类型及成员。但是,反射也存在如下缺点:

    • 反射造成编译时无法保证类型安全
    • 反射速度慢。

    基于上述原因,应尽量避免使用反射来访问字段或调用方法及属性。在设计支持加载项的应用程序时,让类型实现编译时已知的接口,在运行时构造类型的实例,将对它的引用放到接口类型的变量中,再调用接口定义的方法。

    创建支持加载项的应用程序

    1. 添加CoreLib项目,创建ISayHello接口并为接口定义SayHello方法

       namespace ISayHello
       {
           public interface ISayHello
           {
               void SayHello();
           }
       }
      
    2. 修改AssemblyLoad,添加CoreLib引用,并使其中的ClassA、ClassB、ClassC分别实现接口ISayHello,重新编译,将AssemblyLoad.dll文件拷贝至D:DLLAssemblyLoad.dll

       using CoreLib;
      
       namespace AssemblyLoad
       {
           public class ClassA : ISayHello
           {
               public void SayHello()
               {
                   Console.WriteLine("Hello.This is ClassA");
               }
           }
       
           public class ClassB : ISayHello
           {
               public void SayHello()
               {
                   Console.WriteLine("Hello.This is ClassB");
               }
           }
       
           public class ClassC : ISayHello
           {
               public void SayHello()
               {
                   Console.WriteLine("Hello.This is ClassC");
               }
           }
       }
      
    3. 回到控制台应用程序AssemblyAndReflection,添加CoreLib引用,向App.config中添加配置节:

      <appSettings>
        <add key="Test" value="ClassA"/>
      </appSettings>

      修改Program中的Main方法:

       class Program
       {
           static void Main(string[] args)
           {
       		string type = ConfigurationManager.AppSettings["Test"];
               Assembly assembly = Assembly.LoadFrom(@"D:DLLAssemblyLoad.dll");
      
               var q = from r in assembly.ExportedTypes
                   where r.IsClass && typeof(ISayHello).GetTypeInfo().IsAssignableFrom(r.GetTypeInfo()) && r.Name == type
                   select r;
      
               foreach (var t in q)
               {
                   ISayHello s = (ISayHello)Activator.CreateInstance(t);
                   s.SayHello();
               }
      
               Console.ReadLine();
           }
       }
      

    启动程序,控制台输出Hello.This is ClassA,以上示例完成了根据配置文件中的参数调用指定类型下的方法,当然,也可以把参数放在数据库中。

    项目结构示意图:



    使用反射获取类型的成员


    获取类型的成员

      抽象基类System.Reflection.MemberInfo封装了所有类型成员都通用的一组属性。MemberInfo有许多派生类,每个都封装了与特性类型成员相关的更多属性,以下是这些类型的层次结构图:

    调用类型的成员:

    成员类型 调用(Invoke)成员需要调用的方法
    FieldInfo 调用GetValue获取字段的值
    调用SetValue设置字段的值
    ConstructorInfo 调用Invoke构造类型的实例并调用构造器
    MethodInfo 调用Invoke来调用类型的方法
    PropertyInfo 调用GetValue获取属性的get访问器方法
    调用SetValue获取属性的set访问器方法
    EventInfo 调用AddEventHandler来调用事件的add访问器方法
    调用RemoveEventHandler来调用时间的remove访问器方法

    使用绑定句柄减少进程的内存消耗

      Type和MemberInfo类型的对象需要大量的内存,如果将这些对象保存在集合当中,可能对程序的性能产生负面的影响。如果需要保存/缓存大量的Type和MemberInfo对象,可以使用运行时句柄代替对象以减少占用的内存。System命名空间下有三个运行时句柄类型:

    • RuntimeTypeHandle
    • RuntimeFieldHandle
    • RuntimeMethodHandle

    以下是《CLR Via C#》第4版中的示例(博主已经想不到更贴切的示例了):

    class Program
    {
        private const BindingFlags c_bf = BindingFlags.FlattenHierarchy |
            BindingFlags.Instance | BindingFlags.Static |
            BindingFlags.Public | BindingFlags.NonPublic;
        static void Main(string[] args)
        {
    
            //显示在任何反射操作之前堆的大小
            Show("Before doing anything");
    
            //为MScorlib.dll中的所有方法构建MethodInfo对象缓存
            List<MethodBase> methodInfos = new List<MethodBase>();
            foreach (Type t in typeof(Object).Assembly.GetExportedTypes())
            {
                //跳过所有泛型类型
                if (t.IsGenericTypeDefinition)
                {
                    continue;
                }
    
                MethodBase[] mb = t.GetMethods(c_bf);
                methodInfos.AddRange(mb);
            }
    
            //显示当绑定所有方法之后,方法的个数和堆的大小
            Console.WriteLine("# of methods={0:N0}", methodInfos.Count);
            Show("After building cache of MethodInfo obejcts");
    
            //为所有MethodInfo对象构建RuntimeMethodHandle缓存
            List<RuntimeMethodHandle> methodHandles = methodInfos.ConvertAll<RuntimeMethodHandle>(m => m.MethodHandle);
    
            Show("Holding MethodInfo and RuntimeMethodHandle cache");
            GC.KeepAlive(methodInfos);  //组织缓存被过早垃圾回收
            methodInfos = null;         //现在允许缓存垃圾回收
            Show("After freeing MethodInfo objects");
    
            methodInfos = methodHandles.ConvertAll<MethodBase>(rmh => MethodBase.GetMethodFromHandle(rmh));
    
            Show("Size of heap after re-creating MethodInfo objects");
            GC.KeepAlive(methodHandles);
            GC.KeepAlive(methodInfos);
    
            methodHandles = null;
            methodInfos = null;
    
            Show("After freeing MethodInfos and RuntimeMethodHandles");
    
            Console.ReadLine();
        }
    
        private static void Show(string s)
        {
            Console.WriteLine("Heap size={0,12:N0} - {1}", GC.GetTotalMemory(true), s);
        }
    }
  • 相关阅读:
    并行计算的技术路径
    Qt 中文编码问题记录
    rest_rpc 编译安装和测试 ubuntu18.04
    Qt QPorcess 启动外部程序失败的原因之一
    ubuntu 下 cesium的环境搭建
    Qt 渐变色笔记
    Qt编写的自定义控件为什么在QtDesigner中可见,在QtCreator中不可见
    Qt 编译及自动部署 库 工具集(自动复制生成的库及头文件到指定的安装路径)
    Windows10 OSG 编译安装及集成至Qt
    百度图像识别SDK简单使用
  • 原文地址:https://www.cnblogs.com/Answer-Geng/p/7118335.html
Copyright © 2020-2023  润新知