重温CLR(十七)程序集加载和反射

  本章主要讨论在编译时对一个类型一无所知的状况下,如何在运行时发现类型的信息、建立类型的实例以及访问类型的成员。可利用本章讲述的内容建立动态可扩展应用程序。web

反射使用的典型场景通常是由一家公司建立宿主应用程序,其余公司建立加载项(add-in)来扩展宿主应用程序。宿主不能基于一些具体的加载项来构建和测试,由于加载项由不一样公司建立,并且极有多是在宿主应用程序发布以后才建立的。编程

程序集加载

       咱们知道,JIT编译器将方法的IL代码编译成本机代码时,会查看il代码中引用了哪些类型。在运行时,jit编译器利用程序集的TypeRef和AssemblyRef元数据表来肯定哪个程序集定义了所引用的类型。在AssemblyRef元数据表的记录项中,包含了构成程序集强名称的各个部分。jit编译器获取全部这些部分—包括名称、版本、语言文化和公钥信息(public key token)--并把它们链接成一个字符串。而后,jit编译器尝试将与该标识匹配的程序集加载到AppDomain中(若是尚未加载的话)。若是被加载的程序集是弱命名的,那么表示中就只包含程序集的名称。c#

       在内部,clr使用system.reflection.Assembly类的静态load方法尝试加载这个程序集。该方法在.net sdk文档中时公开的,可调用它显式地将程序集加载到AppDomain中。windows

       在内部,Lad致使clr向程序集应用一个版本绑定重定向策略,并在Gac(全局程序集缓存)中查找程序集。若是没找到,就接着去应用程序的基目录、私有路径子目录和codebase位置查找。若是调用Load时传递的是弱命名程序集,load就不会想程序集应用版本绑定重定向策略,clr也不会去gac查找程序集。若是load找到指定的程序集,会返回对表明已加载的那个程序集的一个Assembly对象的引用。若是没找到,会抛出异常。api

       在大多数动态可扩展应用程序中,Assembly的Load的方法是将程序集加载到AppDomain的首选方式。但它要求实现掌握构成程序集标识的各个部分。开发人员常常须要写一些工具或实用程序来操做程序集,他们都要获取引用了程序集文件路径名(包括文件扩展名)的命令行实参。数组

LoadFrom

       调用Assembly的LoadFrom方法加载指定了路径名的程序集:缓存

public class Assembly{ public static Assembly LoadFrom(string path); }

       在内部,LoadFrom首先调用System.Reflection.AssemblyName类的静态GetAssemblyName方法。该方法打开指定的文件,找到AssemblyRef元数据表的记录项,提取程序集标识信息,而后以一个system.reflection.assemblyName对象的形式返回这些信息。随后,LoadFrom方法在内部调用Assembly的Load方法,将AssemblyName对象传给它。而后,clr应用版本绑定重定向策略,并在各个位置查找匹配的程序集。Load找到匹配程序集会加载它,并返回待办已加载程序集的Assembly对象;LoadFrom方法将返回到这个值。若是Load没有找到匹配的程序集,LoadFrom会加载经过LoadFrom的实参传递的路径中的程序集。固然,若是已加载具备相同标识的程序集,LoadFrom方法就会直接返回表明已加载程序集的Assembly对象。安全

LoadForm方法容许传递一个URL做为实参,以下:app

Assembly a=Assembly.LoadFrom(@”http://xxxxxxxxx.xxxxAssembly.dll”);dom

  若是传递的是一个internet位置,clr会下载文件,把它安装到用户的下载缓存中,再从那儿加载文件。注意,当前必须联网,不然会抛出异常。但若是文件以前已下载过,并且ie被设置为脱机工做,就会使用之前下载的文件。

       VS的Ui设计人员和其余工具通常用的是Assembly的LoadFile方法。这个方法可从任意路径加载程序集,并且能够将具备相同标识的程序集屡次加载到一个AppDomain中。在设计器中对应用程序的ui进行修改,并且用户从新生产了程序集时,便有可能发生这种状况。经过LoadFile加载程序集时,clr不会自动解析任何依赖性问题;你的代码必须向AppDomain的AssemblyResolve事件等级,并让事件回调方法显式地加载加载任何依赖的程序集。

       若是你构建的一个工具只想经过反射来分析程序集的元数据,并但愿确保程序集中的任何代码都不会执行,那么加载程序集的最佳方式就是使用Assembly的ReflectionOnlyLoadFrom方法或者使用Assembly的ReflectionOnlyLoad方法。

  ReflectionOnlyLoadFrom方法加载由路径指定的文件;文件的强名称标识不会获取,也不会在GAC和其余位置搜索文件。ReflectionOnlyLoad方法会在GAC、应用程序基目录、私有路径和codebase指定的位置搜索指定的程序集。但和load方法不一样的是,ReflectionOnlyLoad方法不会应用版本控制策略,因此你指定的是哪一个版本,得到的就是哪一个版本。要自行向程序集标识应用版本控制策略,可将字符串传给AppDomain的ApplyPolicy方法。

       利用反射来分析由这两个方法之一加载的程序集时,代码常常须要向AppDomain的ReflectionOnlyAssemblyResovle事件注册一个回调方法,以便手动加载任何引用的程序集;clr不会自动帮你作这个事情。回调方法被调用时,它必须调用Assembly的ReflectionOnlyLoadFrom或ReflectionOnlyLoad方法来显式加载引用程序集,并返回对程序集的引用。

       注意:进程有人问到程序集卸载的问题。遗憾的是,clr不提供卸载单独程序集的能力。若是clr容许这样作,那么一旦线程从某个方法返回至已卸载的一个程序集的代码,应用程序就会崩溃。健壮性和安全性是clr最优先考虑的目标,若是容许应用程序以这样的一种方式崩溃,就和它的设计初衷背道而驰了。卸载程序集必须卸载包含它的整个AppDomain。

       使用ReflectionOnlyLoadFrom或ReflectionOnlyLoad方法加载的程序集表面上是能够卸载的。毕竟,这些程序集中的代码是不容许执行的。但CLR同样不容许卸载用这两个方法加载的程序集。由于用这两个方法加载了程序集以后,仍然能够利用反射来建立对象,以便引用这些程序集中定义的元数据。

       许多应用程序都是由一个要依赖于众多dll文件的exe文件构成。部署应用程序时,全部文件都必须部署。但有一个技术容许只部署一个exe文件。首先标识出exe文件要依赖的、不是做为.NET Framework一部分不发的全部dll文件。而后将这些dll添加到vs项目中。对于添加的每一个dll,都显式它的属性,将它的“生成操做”更改成“嵌入的资源”。这回致使C#编译器将dll文件嵌入exe文件中,之后就只须要部署这个exe。

       在运行时,clr会找不到依赖的dll程序集。为了解决这个问题,当应用程序初始化时,向AppDomain的ResolveAssembly事件登记一个回调方法,代码大体以下:

private static Assembly ResolveEventHandler(object sender,ResolveEventArgs args) { string dllName=new AssemblyName(args.Name).Name+".dll"; var assem = Assembly.GetExecutingAssembly(); string resourceName = assem.GetManifestResourceNames().FirstOrDefault(c => c.EndsWith(dllName)); if (resourceName==null) { return null;//not found,maybe another handler will find it
 } using (var stream=assem.GetManifestResourceStream(resourceName)) { byte[] assemblyData=new byte[stream.Length]; stream.Read(assemblyData, 0, assemblyData.Length); return Assembly.Load(assemblyData); } }

       如今,线程首次调用一个方法时,若是发现该方法引用了依赖DLL文件中的类型,就会引起一个AssemblyResolve事件,而上述回调代码会找到所需的签入dll资源,并调用assembly的load方法获取一个byte[]实参的重载版原本加载所需的资源。虽然我喜欢将依赖dll嵌入程序集的技术,但要注意这会增大应用程序在运行时的内存消耗。

使用反射构建动态可扩展应用程序

       总所周知,元数据时用一系列的表存储的。生成程序集或模块时,编译器会建立一个类型定义表、一个字段定义表、一个方法定义表以及其余表。利用system.reflection命名空间中包含的类型,能够写代码来反射这些元数据表。实际上,这个命名空间中的类型为程序集或模块中包含的元数据提供了一个对象模型。

       利用对象模型中的类型,能够轻松枚举类型定义元数据表中的全部类型,而针对每一个类型均可获取它的基类型、它实现的接口以及与类型关联的标志。利用system.reflection命名空间中的其余类型,还可解析对应的元数据表来查询类型的字段、方法、属性和事件。还可发现应用于任何元数据实体的定制特性。甚至有些类容许判断引用的程序集;还有一些方法能返回一个方法的il字节流。利用全部这些信息,很容易构建出与Microsoft的ilDasm.exe类似的工具。

       事实上,只有极少数应用程序才须要使用反射类型。若是类库须要理解类型的定义才能提供丰富的功能,就适合使用反射。例如,fcl的序列化机制就是利用反射来判断类型定义了哪些字段。而后,序列化格式器(serialiazation formatter)可获取这些字段的值,把它们写入字节流以便经过internet传送、保存到文件或复制到剪贴板。相似地,在设计期间,microsoft visual studio设计器在web窗体或windows窗体上放置控件时,也利用反射来决定要向开发人员显示的属性。

       在运行时,当应用程序须要从特定程序集中加载特定类型以执行特定任务时,也要使用反射。例如,应用程序可要求用户提供程序集和类型名。而后应用程序可显式加载程序集,构造类型的实例,再调用类型中定义的方法。以这种方式绑定到类型并调用方法称为晚期绑定。(对应的,早期绑定是指在编译时就肯定应用程序要使用的类型和方法)。

 

反射的性能

       反射是至关强大的机制,容许在运行时发现并使用编译时还不了解的类型及成员。可是,他也有下面两个缺点。

1 反射形成编译时没法保证类型安全性。因为反射严重依赖字符串,因此会丧失编译时的类型安全性。例如,执行type.getType(“int”);要求经过反射在程序集中查找名为int的类型,代码会经过编译,但在运行时会返回null,由于clr只知道system.int32,不知道int。

2 反射速度慢。使用反射时,类型及其成员的名称在编译时未知;你要用字符串名称标识每一个类型及成员,而后再运行时发现它们。也就是说,使用system.reflection命名空间中的类型扫描程序集的元数据时,反射机制会不停执行字符串搜索。一般,字符串搜索执行的是不区分大小写的比较,这回进一步影响速度。

       使用反射调用成员也会影响性能。用反射调用方法时,首先必须将实参打包成数组;在内部,反射必须将这些实参解包到线程栈上。此外,在调用方法前,clr必须检查实参具备正确的数据类型。最后,clr必须确保调用者有证券的安全权限来访问被调用成员。

       基于上市全部缘由,最好避免利用反射来访问字段或调用方法/属性。应该利用如下两种技术之一开发应用程序来动态发现和构造类型实例。

1 让类型从编译时已知的基类型派生。在运行时构造派生类型的实例,将对它的引用放到基类型的变量中,再调用基类型定义的虚方法。

2 让类型实现编译时已知的接口。在运行时构造类型的实例,将对它的引用放到接口类型的变量中,再调用接口定义的方法。

       在这两种技术中,我我的更喜欢使用接口技术而非基类技术,由于基类技术不容许开发人员选择特定状况下工做得最好的基类。不过,须要版本控制的时候基类技术更合适,由于可随时向基类添加成员,派生类会直接继承该成员。相反,要向接口添加成员,实现该接口的全部类型都得修改它们的代码并从新编译。

      

发现程序集中定义的类型

       反射常常用于判断程序集定义了哪些类型。Fcl提供了许多api来获取这方面的信息。目前经常使用的是assembl的exportedTypes属性

static void Main(string[] args) { string dataAssembly = "System.Data,version=4.0.0.0," + "culture=neutral,PublicKeyToken=b77a5c561934e089"; LoadAssemAndShowPublicTypes(dataAssembly); } private static void LoadAssemAndShowPublicTypes(string assemblyName) { //显式地将程序集加载到这个appDomain中
    Assembly a = Assembly.Load(assemblyName); //在一个循环中显示已加载程序集中每一个公开导出type全面
    foreach (Type t in a.ExportedTypes) { Console.WriteLine(t.FullName); } }

 

类型对象的准确含义

  注意,上述代码遍历system.type对象构成的数组。system.type类型是执行类型和对象操做的起点。system.type对象表明一个类型引用(而不是类型定义)。

       总所周知,system.object定义了公共非虚实例方法getType。调用这个方法时,clr会断定指定对象的类型,并返回对该类型的type对象的引用。因为在一个appDomain中,每一个类型只有一个type对象,因此可使用相等和不相等操做符来判断两个对象是否是相同的类型。

       除了调用object的getType方法,fcl还提供了得到type对象的其余几种方式。

1 system.type类型提供了静态getType方法的几个重载版本。全部版本都接受一个string参数。字符串必须指定类型的全名。

2 system.typeinfo类型提供了实例成员DeclaredNestedTypes和GetDeclaredNestedType。

3 system.reflection.assembly类型提供了实例成员getType,definedtypes和exportedTypes。

       许多编程语言都容许使用一个操做符并根据编译时已知的类型名来得到type对象。尽可能用这个草莝夫获取type引用,而不要使用上述列表中的任何方法,由于操做符生成的代码通畅更快。C#的这个操做符称为typeof,一般用它将晚期绑定的类型信息与早期绑定(编译时已知)的类型信息进行比较。

private static void SomeMethod(object o) { //getType在运行时返回对象的类型(晚期绑定) //typeof返回指定类的类型(早期绑定)
    if (o.GetType()==typeof(FileInfo)) { //.....
 } if (o.GetType()==typeof(DirectoryInfo)) { //.....
 } }

       上述代码的第一个if语句检查变量o是否引用了fileInfo类型的对象;它不检查o是否引用从fileInfo类型派生的对象。换而言之,上述代码测试的是精确匹配,而非兼容匹配。(使用转型或c#的is/as操做符时,测试的就是兼容匹配)。

       如前所述,type对象是轻量级的对象引用。要更多地了解类型自己,必须获取一个typeinfo对象,后者才表明类型定义。可调用system.reflection.introspectionExtensions的getTypeinfo扩展方法将Type对象转换成typeinfo对象。

Type typeReference=…;//例如o.gettype()或者typeof(Object)
TypeInfo typeDefinition=typeReference.getTypeInfo();=

       另外,虽然做用不大,但还可调用TypeInfo的AsType方法将TypeInfo对象转换为Type对象。

TypeInfo typeDefinition=……; Type typeReference = typeDefinition.AsType();

  获取typeInfo对象会强迫clr确保已加载类型的定义程序集,从而对类型进行解析。这个操做可能代价高昂。若是只须要类型引用(type对象),就应该避免这个操做。但一旦得到了typeInfo对象,就可查询类型的许多属性进一步了解它。大多数属性,好比IsPublic,isSealed,isAbstract,isClass和isValueType等,都指明了与类型关联的标志。另外一些属性,好比assembly,assemblyQualifiedName,fullName和module等,则返回定义该类型程序集或模块的名称以及类型全名。还可查询baseType属性来获取对类型的基类型的引用。除此以外,还有许多方法能提供关于类型的更多信息。

构建exception 派生类型的层次结构

       如下代码使用本章讨论的许多概念将一组程序集加载到Appdomain中,并显示最终从System.exception派生的全部类。

private static void Go() { //显示加载想要反射的程序集
 LoadAssemblies(); //对全部类型进行筛选和排序
    var allTypes = (from a in AppDomain.CurrentDomain.GetAssemblies() from t in a.ExportedTypes where typeof(Exception).GetTypeInfo().IsAssignableFrom(t.GetTypeInfo()) orderby t.Name select t).ToArray(); //生成并显示继承层次结构
    Console.WriteLine(WalkInheritanceHierarchy(new StringBuilder(),0,typeof(Exception),allTypes )); } private static StringBuilder WalkInheritanceHierarchy(StringBuilder sb ,int indent,Type baseType,IEnumerable<Type> allTypes) { string spaces = new String(' ', indent * 3); sb.AppendLine(spaces + baseType.FullName); foreach (var t in allTypes) { if (t.GetTypeInfo().BaseType!=baseType) { continue; } WalkInheritanceHierarchy(sb, indent + 1, t, allTypes); } return sb; } private static void LoadAssemblies() { string[] assemblies = {"System,PublicKeyToken={0}", "System.Core,PublicKeyToken={0}","System.Data,PublicKeyToken={0}","System.Design,PublicKeyToken={1}"}; string ecmaPublicKeyToken = "b77a5c561934e089"; string msPublicKeyToken = "b03f5f7f11d50a3a"; //获取包含system.object的程序集的版本,假定其余全部程序集都是相同的版本
    Version version = typeof(System.Object).Assembly.GetName().Version; //显示加载想要反射的程序集
    foreach (var a in assemblies) { string assemblyIdentity = string.Format(a, ecmaPublicKeyToken, msPublicKeyToken) +
                                  ",Culture=neutral,Version=" + version; Assembly.Load(assemblyIdentity); } }
构建exception 派生类型层次结构

输出以下

 

构造类型的实例

       获取对type派生对象的引用以后,就能够构造该类型的实例了。fcl提供了一下几个机制。

1 system.activator的createInstance方法

       activator类提供了静态createInstance方法的几个重载版本。调用方法时既可传递一个type对象引用,也可传递标识了类型的string。直接获取类型对象的几个版本较为简单。你要为类型的构造器传递一组实参,方法返回新对象的引用。

       用字符串来制定类型的几个版本稍微复杂一些。首先必须指定另外一个字符串来表示定义了类型的程序集。其次,若是正确配置了远程访问选项,这些方法还容许构造远程对象。

2 system.activator的createInstanceForm方法

       activator类还提供了一组静态createInstanceForm方法,他们与createInstance的行为类似,只是必须经过字符串参数来指定类型及其程序集。程序集用assembly的loadForm(而非load)方法加载到调用appDin中。因为都不接受type参数,因此返回的都是一个objectHandle对象引用,必须调用ObjectHandle的unwrap方法进行具体化。

3 system.appdomain的方法

       appdomain类型提供了4个用于构造类型实例的实例方法,包括createInstance,createInstanceFrom和createInstanceFromAndUnwrap。这些方法和行为和activator类的方法类似。区别在于他们都是实例方法,容许指定在哪一个appdomain中构造对象。另外,带unwrap后缀的方法还能简化操做,没必要执行额外的方法调用。

4 system.reflection.constructorInfo的invoke实例方法

       使用一个type对象引用,能够绑定到一个特定的构造器,并获取对构造器的constructorInfo对象的引用。而后,可利用constructorInfo对象引用来调用它的invoke方法。类型老是在调用appdomain中建立,返回的是对新对象的引用。

注意: clr不要求值类型定义任何构造器。activator的createInstance方法容许在不调用构造器的状况下建立值类型的实例。必须调用createInstance方法获取单个type参数的版本或者获取type和boolean参数的版本。

       利用前面列出的机制,可为除数组和委托以外的全部类型建立对象。建立数组须要调用array的静态createInstance方法。全部版本的createInstance方法获取的第一个参数都是对数组元素type的引用。createInstance的其余参数容许指定数组位数维数和上下限的各类组合。建立委托则要调用methodInfo的静态createDelegate方法。全部版本的createDelegate方法获取的第一个参数都是对委托type的引用。createDelegate方法的其余参数容许指定在调用实例方法时应将哪一个对象做为this参数传递。

       否早泛型类型的实例首先要获取对开放类型的引用,而后调用type的MakeGenericType方法并向其传递一个数组(其中包含要做为类型实参使用的类型)。而后,获取返回的type对象并把它传给上面列出的某个方法。

internal sealed class Dictionary<TKey,TValue>{} class Program { static void Main(string[] args) { //获取对泛型类型的类型对象的引用
        Type openType = typeof(Dictionary<,>); //使用Tkey=string、Tvalue=int封闭泛型类型 
        Type closedType = openType.MakeGenericType(typeof(string), typeof(int)); //构造封闭类型的实例
        Object o = Activator.CreateInstance(closedType); //证明能正常工做
 Console.WriteLine(o.GetType()); } }

运行结果

ConsoleApp2.Dictionary`2[System.String,System.Int32]

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

       详见原书,这块我理解的不深。

 

使用反射发现类型的成员

       到目前为止,本章的重点一直都是构建动态可扩展应用程序所需的反射机制,包括程序集加载、类型发现以及对象构造。要得到好的性能和编译时的类型安全性,应尽可能避免使用反射。若是是动态可扩展应用程序,构造好对象后,宿主代码通常要将对象转型为编译时已知的接口类型或者基类。这样访问对象的成员就能够得到较好的新能,并且能够确保编译时的类型安全性。

       本章剩余部分将从其余角度探讨反射,目的是发现并调用类型的成员。通常利用这个功能建立开发工具和实用程序,查找特定编程模式或者对特定成员的使用,从而对程序集进行分析。例子包括ilDasm,visual studio的wpf设计器。另外,一些类库也利用这个功能发现和调用类型的成员,为开发人员提供便利和丰富的功能。

发现类型的成员

       字段、构造器、方法、属性、事件和嵌套类型均可以定义成类型的成员。fcl包含抽象基类system.reflection.memberInfo,封装了全部类型成员都通用的一组属性。

 

       如下程序演示了如何查询类型的成员并显示成员的信息。代码处理的是由调用AppDomain加载的全部程序集定义的全部公共类型。对每一个类型都调用DeclaredMembers属性以返回由MemberInfo派生对象构成的集合:每一个对象都引用类型中定义的一个成员。而后,显示每一个成员的种类(字段、构造器、方法和属性等)及其字符串值。

static void Main(string[] args) { //遍历这个appDomain中加载的全部程序集
    Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); foreach (var a in assemblies) { Show(0,"Assembly:{0}",a); //查找程序集中的类型
        foreach (var t in a.ExportedTypes) { Show(1,"Assembly:{0}",t); //发现类型的成员
            foreach (var mi in t.GetTypeInfo().DeclaredMembers) { string typeName = string.Empty; if (mi is Type) { typeName = "(Nested) Type"; } if (mi is FieldInfo) { typeName = "FieldInfo"; } if (mi is MethodInfo) { typeName = "MethodInfo"; } if (mi is ConstructorInfo) { typeName = "ConstructorInfo"; } if (mi is PropertyInfo) { typeName = "PropertyInfo"; } if (mi is EventInfo) { typeName = "EventInfo"; } Show(2,"{0}:{1}",typeName,mi); } } } } private static void Show(int indent,string format,params object[] args) { Console.WriteLine(new string(' ',3*indent)+format,args); }
View Code

输出结果

 

       因为memberInfo类是成员层次结构的根,因此有必要更深刻地研究一下它。下表展现了memberInfo类提供的几个只读属性和方法。这些属性和方法是一个类型的全部成员都通用的。不要忘了system.typeInfo从memberInfo派生。

       在查询declaredMembers属性所返回的集合中,每一个元素都是对层次结构中的一个具体类型的引用。虽然TypeInfo的declaredMembers属性能返回类型的全部成员,但还可利用TypeInfo提供的一些方法返回具备指定字符串名称的成员类型。例如,利用TypeInfo的GetDeclaredNestedType、GetDeclaredField等

       下图总结了用于遍历反射对象模型的各类类型。基于AppDomain,可发现其中加载的全部程序集。基于程序集,可发现构成它的全部模块。基于程序集或模块,可发现它定义的全部类型。基于类型,可发现它的嵌套类型、字段、构造器、方法、属性和事件。命名空间不是这个层次结构的一部分,由于它们只是从语法角度将相关类型汇集到一块儿。clr不知道什么是命名空间。要列出程序集中定义的全部命名空间,需枚举程序集中的全部类型,并查看其namespace属性。

       基于一个类型,还可发现它实现的接口。基于构造器、方法、属性访问器方法或者事件的添加、删除方法,可调用GetParameters方法来获取由parameterInfo对象构成的数组,从而了解成员的参数的类型。还可查询只读属性ReturnParameter得到一个parameterInfo对象,他详细描述了成员的返回类型。对于泛型类型或方法,可调用GetgenericArguments方法来得到类型参数的集合。最后,针对上述任何一项,均可查询customAttributes属性来得到应用于它们的自定义定制特性的集合。

调用类型的成员

       发现类型定义的成员后可调用它们。

 

     PropertyInfo类表明与属性有关的元数据信息;也就是说,PropertyInfo提供了canRead、canWrite和PropertyType只读属性,他们指出属性是否可读和可写,以及属性的数据类型是什么。PropertyInfo还提供了只读getMethod和SetMethod属性,他们返回待办属性get和set访问器方法的MethodInfo对象。PropertyInfo的getValue和setValue方法只是为了提供方便:在内部,,他们会本身调用合适的methodInfo对象。为了支持有参属性(c#的索引器),getValue和setValue方法提供了一个object[]类型的index参数。

       EventInfo类型表明与事件有关的元数据信息。EventInfo类型提供了只读EventHandlerType属性,返回事件的基础委托的type。EventInfo类型还提供了只读addMethod和RemoveMethod属性,返回为事件增删委托的方法的methodInfo对象。增删委托可调用这些MethodInfo对象,也可调用EventInfo类型提供的更好用的addEventHandler和removeEventHandler方法。

       一下实例应用程序演示了用反射来访问类型成员的各类方式。SomeType类包含多种成员:一个私有字段(m_someField);一个公共构造器(someType),它获取一个传引用的int实参;一个公共方法(tostring);一个公共属性(someProp);以及一个公共事件(someEvent)。定义好someType类型后,我提供了三个不一样的方法,他们利用反射来访问someType的成员。三个方法用不一样的方式作相同的事情。

1 BindToMemberThenInvokeTheMember方法演示了如何绑定到成员并调用它。

2 BindToMemberCreateDelegateToMemberThenInvokeTheMember方法演示了如何绑定到一个对象或成员,而后建立一个委托来引用该对象或成员。经过委托来调用的速度很快。若是须要在相同的对象上屡次调用相同的成员,这个技术的性能比上一个好。

3 UseDynamicToBindAndInvokeTheMember方法演示了如何利用C#的dynamic基元类型简化成员访问语法。此外,在相同类型的不一样对象上调用相同成员时,这个计数还能提供不错的性能,由于针对每一个类型,绑定都只会发生一次。并且能够缓存起来,之后屡次调用的速度会很是快。用这个计数也能够调用不一样类型的对象的成员。

internal sealed class SomeType { private int m_someField; public SomeType(int x) { x *= 2; } public override string ToString() { return m_someField.ToString(); } public int SomeProp { get { return m_someField; } set { if (value<1) { throw new ArgumentOutOfRangeException("value"); } m_someField = value; } } public event EventHandler SomeEvent; private void NoCompilerWarnings() { SomeEvent.ToString(); } } class Program { static void Main(string[] args) { Type t = typeof(SomeType); BindToMemberThenInvokeTheMember(t); Console.WriteLine(); BindToMemberCreateDelegateToMemberThenInvokeTheMember(t); Console.WriteLine(); UseDynamicToBindAndInvokeTheMember(t); Console.WriteLine(); } private static void BindToMemberThenInvokeTheMember(Type t) { Console.WriteLine("BindToMemberThenInvokeTheMember"); //构造实例
        Type ctorArgument = Type.GetType("System.Int32"); //或者typeof(Int32).MakeByRefType();
 IEnumerable<ConstructorInfo>  ctors = t.GetTypeInfo().DeclaredConstructors; ConstructorInfo ctor =ctors.First(c => c.GetParameters()[0].ParameterType == ctorArgument); //ConstructorInfo ctor = t.GetTypeInfo().DeclaredConstructors // .First(c => c.GetParameters()[0].ParameterType == ctorArgument);
        object[] args=new object[]{12};//构造器的实参
 Console.WriteLine("x before constructor called:"+args[0]); object obj = ctor.Invoke(args); Console.WriteLine("Type"+obj.GetType()); Console.WriteLine("x after constructor returns"+args[0]); //读写字段
        FieldInfo fi = obj.GetType().GetTypeInfo().GetDeclaredField("m_someField"); fi.SetValue(obj,33); Console.WriteLine("someField:"+fi.GetValue(obj)); //调用方法
        MethodInfo mi = obj.GetType().GetTypeInfo().GetDeclaredMethod("ToString"); string s = (string) mi.Invoke(obj, null); Console.WriteLine("ToString:"+s); //读写属性
        PropertyInfo pi = obj.GetType().GetTypeInfo().GetDeclaredProperty("SomeProp"); try { pi.SetValue(obj,0,null); } catch (TargetInvocationException e) { if (e.InnerException.GetType()!=typeof(ArgumentOutOfRangeException)) { throw; } Console.WriteLine("Property set catch "); } pi.SetValue(obj,2,null); Console.WriteLine("SomeProp:"+pi.GetValue(obj,null)); //为事件添加和删除委托
        EventInfo ei = obj.GetType().GetTypeInfo().GetDeclaredEvent("SomeEvent"); EventHandler eh=new EventHandler(EventCallback); ei.AddEventHandler(obj,eh); ei.RemoveEventHandler(obj,eh); } //添加到事件的回调方法
    private static  void EventCallback(object sender,EventArgs e){} private static void BindToMemberCreateDelegateToMemberThenInvokeTheMember(Type t) { Console.WriteLine("BindToMemberCreateDelegateToMemberThenInvokeTheMember"); //构造实例()不能建立对构造器的委托
            Object[] args=new object[]{12}; Console.WriteLine("x before constructor called:"+args[0]); object obj = Activator.CreateInstance(t,args); Console.WriteLine("Type"+obj.GetType()); Console.WriteLine("x after constructor returns"+args[0]); //注意:不能建立对字段的委托 //调用方法
            MethodInfo mi = obj.GetType().GetTypeInfo().GetDeclaredMethod("ToString"); var toString =  mi.CreateDelegate<Func<string>>(obj); string s = toString(); Console.WriteLine("ToString:"+s); //读写属性
            PropertyInfo pi = obj.GetType().GetTypeInfo().GetDeclaredProperty("SomeProp"); var setSomeProp = pi.SetMethod.CreateDelegate<Action<int>>(obj); try { setSomeProp(0); } catch (ArgumentOutOfRangeException) { Console.WriteLine("Property set catch "); } setSomeProp(2); var getSomeProp=pi.GetMethod.CreateDelegate<Func<int>>(obj); Console.WriteLine("SomeProp:"+getSomeProp()); //为事件添加和删除委托
            EventInfo ei = obj.GetType().GetTypeInfo().GetDeclaredEvent("SomeEvent"); var addSomeEvent = ei.AddMethod.CreateDelegate<Action<EventHandler>>(obj); addSomeEvent(EventCallback); var removeSomeEvent = ei.RemoveMethod.CreateDelegate<Action<EventHandler>>(obj); removeSomeEvent(EventCallback); } private static void UseDynamicToBindAndInvokeTheMember(Type t) { //构造实例()不能建立对构造器的委托
        Object[] args = new object[] { 12 }; Console.WriteLine("x before constructor called:" + args[0]); dynamic obj = Activator.CreateInstance(t, args); Console.WriteLine("Type" + obj.GetType()); Console.WriteLine("x after constructor returns" + args[0]); //读写字段

        try { obj.m_someField = 5; int v=(int) obj.m_someField; Console.WriteLine("someField:"+v); } catch (RuntimeBinderException e) { Console.WriteLine("failed to access field: "+e.Message); } //调用方法
        
        string s = (string)obj.ToString(); Console.WriteLine("ToString:" + s); //读写属性
        try { obj.SomeProp=0; } catch (ArgumentOutOfRangeException e) { Console.WriteLine("Property set catch "); } obj.SomeProp=2; int val =(int)obj.SomeProp; Console.WriteLine("SomeProp:" + val); //为事件添加和删除委托
        obj.SomeEvent+=new EventHandler(EventCallback); obj.SomeEvent-=new EventHandler(EventCallback); } } internal static class ReflectionExtensions { public static TDelegate CreateDelegate<TDelegate>(this MethodInfo mi,object target=null) { return (TDelegate) (Object) mi.CreateDelegate(typeof(TDelegate), target); } }
反射来访问类型成员的各类方式

运行输出

BindToMemberThenInvokeTheMember x before constructor called:12 TypeConsoleApp2.SomeType x after constructor returns12 someField:33 ToString:33 Property set catch SomeProp:2 BindToMemberCreateDelegateToMemberThenInvokeTheMember x before constructor called:12 TypeConsoleApp2.SomeType x after constructor returns12 ToString:0 Property set catch SomeProp:2 x before constructor called:12 TypeConsoleApp2.SomeType x after constructor returns12 failed to access field: “ConsoleApp2.SomeType.m_someField”不可访问,由于它具备必定的保护级别 ToString:0 Property set catch SomeProp:2 请按任意键继续. . .

 

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

       许多应用程序都绑定了一组类型(Type对象)或类型成员(MemberInfo派生对象),并将这些对象保存在某种形式的集合中。之后,应用程序搜索这个集合,查找特定对象,而后调用(invoke)这个对象。这个机制很好,只是有个小问题:type和memberinfo派生对象须要大量内存。因此,若是应用程序容纳了太多这样的对象,但只是偶尔调用,应用程序消耗的内存就会急剧增长,对应用程序的性能产生负面影响。

       clr内部用更精简的方式表示这种信息。clr之因此为应用程序建立这些对象,只是为了方便开发人员。clr不须要这些大对象就能运行。若是须要保存/缓存大量type和memberinfo派生对象,开发人员可使用句柄(runtime handle)代替对象以减少工做集内存。FCL定义了三个运行时句柄类型(所有都在system命名空间),包括RuntimeTypeHandle,RuntimeFieldHandle和RuntimeMethodHandle。三个类型都是值类型,都只包含一个字段,也就是一个IntPtr;这使类型的实例显得至关精简。intPtr字段是一个句柄,引用AppDomain的Loader堆中的一个类型、字段或方法。所以,如今须要以一种简单、搞笑的方式将重量级的type或memberInfo对象转换为轻量级的运行时句柄实例,反之亦然。幸亏,使用如下转换方法和属性可轻松达到目的。

如下实例程序获取许多methodInfo对象,把它们转换为RuntimeMethodHandle实例,并演示了转换先后的工做集的差别。

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 buiding cache of MethodInfo objects"); //为全部methodINFO对象构建RuntimeMethodHandle缓存
    List<RuntimeMethodHandle> methodHandles = methodInfos.ConvertAll<RuntimeMethodHandle>(c => c.MethodHandle); //GC.KeepAlive(methodInfos);//阻止缓存被过早垃圾回收
    Show("Holding MethodInfo And RuntimeMethodHandle CACHE"); //methodInfos = null;//如今容许缓存垃圾回收 //GC.Collect();
 methodInfos = methodHandles.ConvertAll<MethodBase>(c => MethodBase.GetMethodFromHandle(c)); Show("size of heap after re_createing methodInfo objects"); methodInfos = null;//如今容许缓存垃圾回收
    Show("After freeing methodinfo objects"); GC.KeepAlive(methodHandles);//组织缓存被过早垃圾回收 //GC.KeepAlive(methodInfos);//组织缓存被过早垃圾回收
    methodHandles = null; //methodInfos = null;//如今容许缓存垃圾回收
 Show("after freeing methodInfos and RuntimeMethodHandles"); Console.ReadKey(); } private static void Show(string s) { Console.WriteLine("Heap size={0,2:N0}-{1}",GC.GetTotalMemory(true),s); }
View Code

输出以下

Heap size=22,440-before doing anything # OF methods=54,346 Heap size=4,102,652-after buiding cache of MethodInfo objects Heap size=4,320,108-Holding MethodInfo And RuntimeMethodHandle CACHE Heap size=4,181,160-size of heap after re_createing methodInfo objects Heap size=3,963,736-After freeing methodinfo objects Heap size=89,824-after freeing methodInfos and RuntimeMethodHandles
相关文章
相关标签/搜索