第 23 章 程序集加载和反射

第 23 章 程序集加载和反射

本章内容

23.1 程序集加载

JIT 编译器将方法的 IL 代码编译成本机代码时,会查看 IL 代码中引用了哪些类型。在运行时,JIT 编译器利用程序集的 TypeRef 和 AssemblyRef 元数据表来确定哪一个程序集定义了所引用的类型。在 AssemblyRef 元数据表的记录项中,包含了构成程序集强名称的各个部分。JIT 编译器尝试将与该标识匹配的程序集加载到 AppDomain 中(如果还没有加载的话)。

CLR 使用 System.Reflection.Assembly 类的静态 Load 方法尝试加载这个程序集。

1
2
3
4
5
public class Assembly {
public static Assembly Load(AssemblyName assemblyRef);
public static Assembly Load(String assemblyString);
// 未列出不常用的 Load 重载
}

如果你构建的一个工具只想通过反射来分析程序集的元数据,并希望确保程序集中的任何代码都不会执行,那么加载程序集的最佳方式就是使用 AssemblyReflectionOnlyLoadFrom 方法或者使用 AssemblyReflectionOnlyLoad 方法(后者比较少见)。下面是这两个方法的原型:

1
2
3
4
5
public class Assembly {
public static Assembly ReflectionOnlyLoadFrom(String assemblyFile);
public static Assembly ReflectionOnlyLoad(String assemblyString);
// 未列出不常用的 ReflectionOnlyLoad 重载
}

ReflectionOnlyLoadFromReflectionOnlyLoad 方法加载程序集时,CLR 禁止程序集中的任何代码执行

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

众所知周,元数据是用一系列表来存储的。生成程序集或模块时,编译器会创建一个类型定义表、一个字段定义表、一个方法定义表以及其他表。利用 System.Reflection 命名空间中的其他类型,可以写代码来反射这些元数据表。

23.3 反射的性能

反射是相当强大的机制,允许在运行发现并使用编译时还不了解的类型及其成员。但是,它也有下面两个缺点。

  • 反射造成编译时无法保证类型安全性。由于反射严重依赖字符串,所以会丧失编译时的类型安全性。
  • 反射速度慢。使用 System.Reflection 命名空间中的类型扫描程序集的元数据时,反射机制会不停地执行字符串搜索。

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

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

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

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

23.3.1 发现程序集中定义的类型

目前最常用的 API 是 AssemblyExportedTypes 属性。下例加载一个程序集,并显示其中定义的所有公开导出的类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
using System;
using System.Reflection;

public static class Program {
public static void Main() {
String dataAssembly = "System.Data, version=4.0.0.0, "
+ "culture=neutral, PublicKeyToken=b77a5c561934e089";
LoadAssemAndShowPublicTypes(dataAssembly);
}

private static void LoadAssemAndShowPublicTypes(String assemId) {
// 显式地将程序集加载到这个 AppDomain 中
Assembly a = Assembly.Load(assemId);

// 在一个循环中显示已加载程序集中每个公开导出 Type 的全名
foreach (Type t in a.ExportedTypes) {
// 显示类型全名
Console.WriteLine(t.FullName);
}
}
}

23.3.4 构造类型的实例

获得对 Type 派生对象的引用之后,就可以构造该类型的实例了。FCL 提供了以下几个机制。

  • System.ActivatorCreateInstance 方法

  • System.ActivatorCreateInstanceFrom 方法

  • System.AppDomain 的方法

    AppDomain 类型提供了 4 个用于构造类型实例的实例方法(每个都有几个重载版本),包括 CreateInstanceCreateInstanceAndUnwrapCreateInstanceFromCreateInstanceFromAndUnwrap。这些方法的行为和 Activator 类的方法相似,区别在于它们都是实例方法,允许指定在哪个 AppDomain 中构造对象。

  • System.Reflection.ConstructorInfoInvoke 实例方法

创建数组需要调用 Array 的静态 CreateInstance 方法。所有版本的 CreateInstance 方法获取的第一个参数都是对数组元数 Type 的引用。CreateInstance 的其他参数允许指定数组维数和上下限的各种组合。

创建委托则要调用 MethodInfo 的静态 CreateDelegate 方法。所有版本的 CreateDelegate 方法获取的第一个参数都是对委托 Type 的引用。CreateDelegate 方法的其他参数允许指定在调用实例方法时应将哪个对象作为 this 参数传递。

构造泛型类型的实例首先要获取对开放类型的引用,然后调用 TypeMakeGenericType 方法并向其传递一个数组(其中包含要作为类型实参使用的类型)。然后,获取返回的 Type 对象并把它传给上面列出的某个方法。下面是一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
using System;
using System.Reflection;

internal sealed class Dictionary<Tkey, TValue> {}

public static class Program {
public static void Main() {
// 获取对泛型类型的类型对象的引用
Type openType = typeof(Dictionary<,>);

// 使用 TKey=String、TValue=Int32 封闭泛型类型 ①
Type closedType = openType.MakeGenericType(typeof(String), typeof(Int32));

// 构造封闭类型的实例
Object o = Activator.CreateInstance(closedType);

// 证实能正常工作
Console.WriteLine(o.GetType());
}
}

23.5 使用反射发现类型的成员

23.5.1 发现类型的成员

字段、构造器、方法、属性、事件和嵌套类型都可以定义成类型的成员。FCL 包含抽象基类 System.Reflection.MemberInfo ,封装了所有类型成员都通用的一组属性。MemberInfo 有许多派生类,每个都封装了与特定类型成员相关的更多属性。图 23-1 是这些类型的层次结构。

23_1
表 23-1 MemberInfo 的所有派生类型都通用的属性和方法

成员名称 成员类型 说明
Name 一个 String 属性 返回成员名称
DeclaringType 一个 Type 属性 返回声明成员的 Type
Module 一个 Module 属性 返回声明成员的 Module
CustomAttributes 该属性返回一个 IEnumerable<CustomAttributeData> 返回一个集合,其中每个元素都标识了应用于该成员的一个定制特性的实例。定制特性可应用于任何成员。虽然 Assembly 不从 MemberInfo 派生,但它提供了可用于程序集的相同属性

图 23-2 总结了用于遍历反射对象模型的各种类型。基于 AppDomain,可发现其中加载的所有程序集。基于程序集,可发现构成它的所有模块。基于程序集或模块,可发现它定义的所有类型。基于类型,可发现它的嵌套类型、字段、构造器、方法、属性和事件。

23_2

  • 基于一个类型,还可发现它实现的接口。

  • 基于构造器、方法、属性访问器方法或者事件的添加/删除方法,可调用 GetParameters 方法来获取由 ParameterInfo 对象构成的数组,从而了解成员的参数的类型。还可查询只读属性 ReturnParameter 获得一个 ParameterInfo 对象,它详细描述了成员的返回类型。

  • 对于泛型类型或方法,可调用 GetGenericArguments 方法来获得类型参数的集合。

  • 最后,针对上述任何一项,都可查询 CustomAttributes 属性来获得应用于它们的自定义定制特性的集合。

23.5.2 调用类型的成员

发现类型定义的成员后可调用它们。表 23-2 展示了为了调用一种成员而需调用的方法。
表 23-2 如何调用成员

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

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

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

CLR 不需要这些大对象就能运行。开发人员可以使用``运行时句柄(runtime handle)代替对象以减小工作集(占用的内存)。FCL 定义了三个运行时句柄(全部都在 System命名空间中),包括RuntimeTypeHandleRuntimeFieldHandleRuntimeMethodHandle。三个类型都是值类型,都只包含一个字段,也就是一个 IntPtr;这使类型的实例显得相当精简(相当省内存)。IntPtr字段是一个句柄,引用了 AppDomain 的 Loader 堆中的一个类型、字段或方法。因此,现在需要以一种简单、高效的方式将重量级的TypeMemberInfo` 对象转换为轻量级的运行时句柄实例,反之亦然。幸好,使用以下转换方法和属性可轻松达到目的。

  • 要将 Type 对象转换为一个 RuntimeTypeHandle,调用 Type 的静态 GetTypeHandle 方法并传递那个 Type 对象引用。

  • 要将一个 RuntimeTypeHandle 转换为 Type 对象,调用 Type 的静态方法 GetTypeFromHandle,并传递那个 RuntimeTypeHandle

  • 要将 FieldInfo 对象转换为一个 RuntimeFieldHandle,查询 FieldInfo 的实例只读属性 FieldHandle

  • 要将一个 RuntimeFieldHandle 转换为 FieldInfo 对象,调用 FieldInfo 的静态方法 GetFieldFromHandle

  • 要将 MethodInfo 对象转换为一个 RuntimeMethodHandle,查询 MethodInfo 的实例只读属性 MethodHandle

  • 要将一个 RuntimeMethodHandle 转换为一个 MethodInfo 对象,调用 MethodInfo 的静态方法 GetMethodFromHandle