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

时间:2022-01-26 08:44:44

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

反射使用的典型场景一般是由一家公司创建宿主应用程序,其他公司创建加载项(add-in)来扩展宿主应用程序。宿主不能基于一些具体的加载项来构建和测试,因为加载项由不同公司创建,而且极有可能是在宿主应用程序发布之后才创建的。

程序集加载

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

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

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

在大多数动态可扩展应用程序中,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作为实参,如下:

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

  如果传递的是一个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, , 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(),,typeof(Exception),allTypes ));
}
private static StringBuilder WalkInheritanceHierarchy(StringBuilder sb ,int indent,Type baseType,IEnumerable<Type> allTypes)
{
string spaces = new String(' ', indent * );
sb.AppendLine(spaces + baseType.FullName);
foreach (var t in allTypes)
{
if (t.GetTypeInfo().BaseType!=baseType)
{
continue;
}
WalkInheritanceHierarchy(sb, indent + , 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 派生类型层次结构

输出如下

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

构造类型的实例

获取对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,封装了所有类型成员都通用的一组属性。

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

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

static void Main(string[] args)
{
//遍历这个appDomain中加载的所有程序集
Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();
foreach (var a in assemblies)
{
Show(,"Assembly:{0}",a);
//查找程序集中的类型
foreach (var t in a.ExportedTypes)
{
Show(,"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(,"{0}:{1}",typeName,mi);
}
}
}
}
private static void Show(int indent,string format,params object[] args)
{
Console.WriteLine(new string(' ',*indent)+format,args);
}

输出结果

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

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

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

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

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

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

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

调用类型的成员

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

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

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 *= ;
}
public override string ToString()
{
return m_someField.ToString();
}
public int SomeProp
{
get { return m_someField; }
set
{
if (value<)
{
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()[].ParameterType == ctorArgument);
//ConstructorInfo ctor = t.GetTypeInfo().DeclaredConstructors
// .First(c => c.GetParameters()[0].ParameterType == ctorArgument);
object[] args=new object[]{};//构造器的实参 Console.WriteLine("x before constructor called:"+args[]);
object obj = ctor.Invoke(args);
Console.WriteLine("Type"+obj.GetType());
Console.WriteLine("x after constructor returns"+args[]); //读写字段
FieldInfo fi = obj.GetType().GetTypeInfo().GetDeclaredField("m_someField");
fi.SetValue(obj,);
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,,null);
}
catch (TargetInvocationException e)
{
if (e.InnerException.GetType()!=typeof(ArgumentOutOfRangeException))
{
throw;
}
Console.WriteLine("Property set catch ");
}
pi.SetValue(obj,,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[]{}; Console.WriteLine("x before constructor called:"+args[]);
object obj = Activator.CreateInstance(t,args);
Console.WriteLine("Type"+obj.GetType());
Console.WriteLine("x after constructor returns"+args[]); //注意:不能创建对字段的委托 //调用方法
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();
}
catch (ArgumentOutOfRangeException)
{
Console.WriteLine("Property set catch ");
}
setSomeProp();
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[] { }; Console.WriteLine("x before constructor called:" + args[]);
dynamic obj = Activator.CreateInstance(t, args);
Console.WriteLine("Type" + obj.GetType());
Console.WriteLine("x after constructor returns" + args[]); //读写字段 try
{
obj.m_someField = ;
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=;
}
catch (ArgumentOutOfRangeException e)
{
Console.WriteLine("Property set catch ");
}
obj.SomeProp=;
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:
TypeConsoleApp2.SomeType
x after constructor returns12
someField:
ToString:
Property set catch
SomeProp: BindToMemberCreateDelegateToMemberThenInvokeTheMember
x before constructor called:
TypeConsoleApp2.SomeType
x after constructor returns12
ToString:
Property set catch
SomeProp: x before constructor called:
TypeConsoleApp2.SomeType
x after constructor returns12
failed to access field: “ConsoleApp2.SomeType.m_someField”不可访问,因为它具有一定的保护级别
ToString:
Property set catch
SomeProp:
请按任意键继续. . .

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

许多应用程序都绑定了一组类型(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);
}

输出如下

Heap size=,-before doing anything
# OF methods=,
Heap size=,,-after buiding cache of MethodInfo objects
Heap size=,,-Holding MethodInfo And RuntimeMethodHandle CACHE
Heap size=,,-size of heap after re_createing methodInfo objects
Heap size=,,-After freeing methodinfo objects
Heap size=,-after freeing methodInfos and RuntimeMethodHandles