第三节:反射的性能
反射是相当强大的一个机制,它允许在运行时发现并使用编译时还不了解的类型及其成员。但是,它也有下面两个缺点:
1 、反射会造成编译时无法保证类型的安全性,由于反射要严重依赖于字符串,所以会丧失编译时类型安全。例如:假如执行Type.GetType(“Jef”);要求通过反射在一个程序中查找一个名为”Jef”的类型,但程序集包含的实际是”Jeff”类型,代码会通过编译,但是在运行时会出错,因为作为实参传递的类型名称被错误地拼写。
2、反射速度慢。使用反射时,类型以及成员的名称在编译时未知;要使用字符串名称来标识每个类型以及成员,以便在运行时发现他们。也就是说,使用System.Reflection命名空间中的类型扫描程序集的元数据时,反射要不断的执行字符串搜索。通常,字符串搜索执行时不区分大小写的比较,这回近异步影响速度。
使用反射调用一个成员时,也会对性能产生影响。用反射调用一个方法时,首先必须将实参打包成一个数据,在内部,反射必须将这些实参解包到线程栈上。此外,在调用方法前,CLR必须检查实参具有正确的数据类型。最后,CLR必须确保调用者有正确的安全权限来访问被调用的成员。
基于上诉所有原因,最好避免利用反射来访问字段或者调用方法和属性。如果要写一个应用程序来动态发现和构造类型实例,应采取以下两种技术之一。
1、 让类型从一个已知的基类型派生。在运行时,构造派生类的一个实例,将对他的引用放到基类型的变量中,再调用基类型定义的虚方法。
2、 让类型实现一个编译时已知的接口。在运行时,构造一个类型的实例,将对他的引用放到接口类型的变量中,再调用接口定义的方法。
在这两种技术中,我个人更喜欢接口技术而非基类技术,因为基类技术不允许开发人员选择在一个特定情况下工作的最好的基类,不过,在需要版本控制的情形下,基类的技术显的更合适一些,因为随时都能向基类添加一个成员,派生类会直接继承他。相反,要向接口添加一个成员,实现该接口的所有类型都得修改他们的代码并重新编译。
在使用这两种技术时,强烈建议在接口或基类型自己的程序集中定义他们,有助于缓解版本控制问题。
一、发现程序集中定义的类型
反射经常判断程序集中定义了哪些类型。FCL中提供了好多方面的信息。到目前为止,最常见的方法时Assembly的GetExportedTypes.下面加载一个程序集,并显示其中定义的所有公开导出的类型。
private static void LoadAssemAndShowPublicTypes(string assemid) { Assembly a = Assembly.Load(assemid); foreach (Type t in a.GetExportedTypes()) { Console.WriteLine(t.FullName); } }
二、类型对象的准确含义
注意,上述代码遍历System.Type对象构成的一个数组。System.Type类型时执行类型和对象操作的起点。System.Type时一个从System.Reflection.MemberInfo派生的抽象基类,FCL中定义了几个从System.Type派生的基类,包括System.RuntimeType,System.ReflectionOnlyType,System.Reflection.TypeDelegator以及System.Reflection.Emit命名空间中的一些类型。
所有这些类型中,System.RuntimeType是最有趣的一个。这个类型时FCL内部使用的一些类型。所以在FCL文档中找不到,一个类型在一个AppDomain中首次访问时,CLR会构造一个RuntimeType的一个实例,并初始化这个RuntimeType对象的字段。
我们知道,System.Object定义了一个公共非虚实例方法GetType。调用这个方法时,CLR会判断指定对象的类型,并返回对它的RuntimeType对象的一个引用。由于在一个AppDomain中,每个类型只有一个RuntimeType对象,所以可以使用相等和不相等操作符来判断两个对象是不是属于同一个类型。
除了调用Object的GetType方法,FCL还提供了获得Type对象的其他几种方式。
1、 System.Type类型提供了静态方法GetType的几个重载版本,每个方法的所有版本都接受一个String参数。这个字符串必须制定类型的全名(包括它的命名空间)。注意,不允许制定编译器支持的基元类型。这些类型对CLR来说没有任何意义。如果传递字符串只是类型全名,方法将检索调用程序集,看他是否定义了制定名称的类型。如果是,就返回对一个恰当RuntimeType对象的引用。
如果调用程序集没有定义制定的类型,就检查MSCorLib.dll定义的类型,如果还是没有找到匹配名称的一个类型,就返回null或抛出System.TypeLoadException。
可想GetType传递一个限定了程序集的类型字符串,比如“System.Int32,mscorlib,Version=4.0.0.0,Culture=neutral,PublicKeyToken=…..”GetType会在制定的程序集中查找类型。
2、 System.Type类型提供了一个静态方法ReflectionOnlyGetType。该方法和提到的GetType方法的执行相似,只是类型回家再到仅反射的上下文,不能执行。
3、 System.Type类型提供了一下实例方法:GetNestedType与GetNestedTypes。
4、 System.Reflection.Module类型提供了以下实例方法:GetType ,GetTypes, FindTypes
注意:我们知道,构建传给反射方法的字符串时,要使用类型名称和限定了程序集的类型名称,微软为这些名称定了巴克斯-诺尔范式语法。使用反射时,了解这些语法会对你有不少好处,尤其是处理嵌套类型,泛型类型,泛型方法,引用参数或者数组时。
许多编程语言还提供一个操作符,允许根据编译时已知的类型名称来获得一个Type对象。如果有可能,应该用这个操作符来获得对一个Type对象的引用,而不是使用上诉列表中的任何方法,因为操作符通常能生成更快的代码。C#的这个操作符称为typeof,通常可用该操作符将晚期绑定的信息和早期绑(编译时已知)定的信息进行比较。一下演示了一个例子:
private static void SomeMethod(Object o)
{
if (o.GetType() == typeof(FileInfo)) { }
if (o.GetType() == typeof(DirectoryInfo)) { }
}
注意:上述代码中的第一个if语句检查变量o是否引用了FileInfo类型的一个对象;它不检查o是否引用从FileInfo类型派生的一个对象。换言之,上面代码检测的是精确匹配,而非兼容匹配。(使用C#的is/as操作符时,测试的就是兼容匹配)
获得对一个Type对象的引用后,就可查询类型的许多属性,更进一步了解该类型,大多数属性,比如IsPublic , IsSealed ,IsAbstract ,IsClass, IsValueType等,即指明了与类型关联的标志。另一些属性,比如:Assembly,AssemblyQualifiedName,FullName,Module等,则返回定义该类型的那个程序集或模块的名称,或返回类型全名,还可查询BaseType属性来获取类型的基类型。除此之外还有许多方法能提供类型的更多信息。
三、构造类型的实例
拥有对一个Type对象的派生类型之后,就可以构造该类型的一个实例。为此,FCL提供了一下几种机制。
1. System.Activator的CreateInstance方法 System.Activator类提供了静态 CreateInstance方法的几个重载版本。调用该方法时,可以传递一个Type对象引用,也可以传递标识了想要创建的类型的一个String。直接获取一个类型对象的几个版本要简单一些,你要为类型的构造器传递一组实参,方法返回是对新对象的一个引用。
使用字符串来指定所需类型的几个版本则稍微复杂一些。首先,必须指定另一个字符串来标识定义了类型的那个程序集。其次,如果正确配置了远程访问选项,这些方法还允许远程构造对象。第三,这些这些版本返回不是对想对象的一个引用,而是一个System.Runtime.Remoting.ObjectHandle对象(派生自System.MarshalByRefObject)。
ObjectHandle类型允许将一个AppDomain中创建的对象传给其他AppDomain,期间不强迫对象具体化。要具体化这个对象,请调用ObjectHandle的Unwrap方法。在一个AppDomain中调用该方法时,它会将定义了要具体化的类型的程序集加载到这个AppDomain中,如果对象按引用封送,会创建代理类型和对象。如果对象按值封送,对象的副本会被反序列化。
2. System.Activator的CreateInstanceFrom方法 Activator类还提供了一组静态CreateInstanceFrom方法。这些方法与CreateInstance方法行为相似。只是必须通过字符串来指定类型以及程序集。程序集要用Assembly的LoadFrom加载到调用AppDomain中。由于这些方法都不接受一个Type参数,所以返回的都是一个ObjectHandle对象引用,必须调用ObjectHandle的Unwrap方法进行具体化。
3. System.AppDomain的方法 AppDomain类型提供了4个用于构造类型实例的实例方法(每一个都有几个重载版本): CreateInstance , CreateInstanceAndUnwrap,CreateInstanceFrom,CreateInstanceFromAndUnwrap。这些方法的行为和Activator类的方法相似,只是他们都是实例方法,允许制定在哪个AppDomain中构造对象。另外,带Unwrap后缀还可以简化我们的一些操作,因为不必在执行一次额外的方法调用。
4.Sytem.Type的InvokeMember实例方法 可以使用一个Type对象的引用来调用InvokeMember方法。该方法会查找与传递的实参匹配的一个构造器,并构造类型。类型总是在调用AppDomain中创建,返回的是对新对象的一个引用。
5. System.Reflection.ConstructorInfo 的Invoke实例方法 使用一个Type对象引用,可以绑定到一个特定的构造器,并获取对构造器ConstructorInfo 对象的一个引用。然后可以利用这个ConstructorInfo 对象的引用来调用它的Invoke方法。类型总是在AppDomain中创建,返回的是对新对象的一个引用。
利用前面列出的机制,可以除了数组(System.Array派生类)和委托(System.MulticastDelegate派生类)之外的所有类型创建一个对象。为了创建一个数组,应该调用Array的静态CreateInstance方法。所有版本的CreateInstance方法获取的第一个参数都是对数组元素Type的一个引用。CreateInstance的其他参数允许制定数组维数和上下限的各种组合。为了创建一个委托,应该调用Delegate的静态CreateDelegate方法。所有版本CreateDelegate方法的第一个参数都是对委托实例Type的一个引用。其他参数允许指定要在委托中包装的一个对象的实例方法(或者一个类型的静态方法)。
为了构造一个泛型类型的实例,首先要获取对开发类型的一个引用,然后调用Type的公共的实例方法MakeGenericType,向它传递一个数组(其中包含了要作为类型实参使用的类型),然后,获取返回Type对象,吧它传给上面列出的某个方法。
internal sealed class Dictionary<TKey, TValue> { } static void Main(string[] args) { Type openType = typeof(Dictionary<,>); Type closeType = openType.MakeGenericType(typeof(String), typeof(Int32)); Object o = Activator.CreateInstance(closeType); Console.WriteLine(o.GetType()); }