# Chapter 23 Assembly Loading and Reflection

# Assembly Loading

As you know, when the just-in-time (JIT) compiler compiles the Intermediate Language (IL) for a method, it sees what types are referenced in the IL code. Then at run time, the JIT compiler uses the assembly’s TypeRef and AssemblyRef metadata tables to determine what assembly defines the type being referenced. The AssemblyRef metadata table entry contains all of the parts that make up the strong name of the assembly. The JIT compiler grabs all of these parts—name (without extension or path), version, culture, and public key token—concatenates them into a string, and then attempts to load an assembly matching this identity into the AppDomain (assuming that it’s not already loaded). If the assembly being loaded is weakly named, the identity is just the name of the assembly (no version, culture, or public key token information).

Internally, the CLR attempts to load this assembly by using the System.Reflection.Assembly class’s static Load method. This method is publicly documented, and you can call it to explicitly load an assembly into your AppDomain. This method is the CLR equivalent of Win32’s LoadLibrary function. There are actually several overloaded versions of Assembly’s Load method. Here are the prototypes of the more commonly used overloads.

public class Assembly { 
 public static Assembly Load(AssemblyName assemblyRef); 
 public static Assembly Load(String assemblyString); 
 // Less commonly used overloads of Load are not shown 
}

Internally, Load causes the CLR to apply a version-binding redirection policy to the assembly and looks for the assembly in the global assembly cache (GAC), followed by the application’s base directory, private path subdirectories, and codebase locations. If you call Load by passing a weakly named assembly, Load doesn’t apply a version-binding redirection policy to the assembly, and the CLR won’t look in the GAC for the assembly. If Load finds the specified assembly, it returns a reference to an Assembly object that represents the loaded assembly. If Load fails to find the specified assembly, it throws a System.IO.FileNotFoundException.

💡注意:一些极罕见的情况可能需要加载为特定 CPU 架构生成的程序集。这时在指定程序集的标识时,还可包括一个进程架构部分。例如,假定 GAC 中同时包含了一个程序集的 IL 中立版本和 x86 专用版本,CLR 会默认选择 x86 专用版本 (参见 第 3 章 “共享程序集和强命名程序集”)。但是,为了强迫 CLR 加载 IL 中立的版本,可以向 AssemblyLoad 方法传递以下字符串:

"SomeAssembly, Version=2.0.0.0, Culture=neutral, PublicKeyToken=01234567890abcde, ProcessorArchitecture=MSIL"

CLR 目前允许 ProcessorArchitecture 取 5 个值之一: MSIL (Microsoft IL)、 X86IA64AMD64 以及 Arm

💡重要提示:一些开发人员可能注意到 System.AppDomain 提供了 Load 方法。和 Assembly 的静态 Load 方法不同, AppDomainLoad 是实例方法,它允许将程序集加载到指定的 AppDomain 中。该方法设计由非托管调用,允许宿主将程序集 “注入” 特定 AppDomain 中。托管代码的开发人员一般情况下不应调用它,因为调用 AppDomainLoad 方法时需要传递一个标识了程序集的字符串。该方法随后会应用策略,并在一些常规位置搜索程序集。我们知道,AppDomain 关联了一些告诉 CLR 如何查找程序集的设置。为了加载这个程序集,CLR 将使用与指定 AppDomain 关联的设置,而非与发出调用之 AppDomain 关联的设置。

AppDomainLoad 方法会返回对程序集的引用。由于 System.Assembly 类不是从 System.MarshalByRefObject 派生的,所以程序集对象必须按值封送回发出调用的那个 AppDomain。但是,现在 CLR 就会用发出调用的那个 AppDomain 的设置来定位并加载程序集。如果使用发出调用的那个 AppDomain 的策略和搜索位置找不到指定的程序集,就会抛出一个 FileNotFoundException 。 这个行为一般不是你所期望的,所以应该避免使用 AppDomainLoad 方法。

In most dynamically extensible applications, Assembly’s Load method is the preferred way of loading an assembly into an AppDomain. However, it does require that you have all of the pieces that make up an assembly’s identity. Frequently, developers write tools or utilities (such as ILDasm.exe, PEVerify.exe, CorFlags.exe, GACUtil.exe, SGen.exe, SN.exe, XSD.exe) that perform some kind of processing on an assembly. All of these tools take a command-line argument that refers to the path name of an assembly file (including file extension).

To load an assembly specifying a path name, you call Assembly’s LoadFrom method.

public class Assembly { 
 public static Assembly LoadFrom(String path); 
 // Less commonly used overloads of LoadFrom are not shown 
}

Internally, LoadFrom first calls System.Reflection.AssemblyName’s static GetAssemblyName method, which opens the specified file, finds the AssemblyDef metadata table’s entry, and extracts the assembly identity information and returns it in a System.Reflection.AssemblyName object (the file is also closed). Then, LoadFrom internally calls Assembly’s Load method, passing it the AssemblyName object. At this point, the CLR applies a version-binding redirection policy and searches the various locations looking for a matching assembly. If Load finds the assembly, it will load it, and an Assembly object that represents the loaded assembly will be returned; LoadFrom returns this value. If Load fails to find an assembly, LoadFrom loads the assembly at the path name specified in LoadFrom’s argument. Of course, if an assembly with the same identity is already loaded, LoadFrom simply returns an Assembly object that represents the already loaded assembly.

By the way, the LoadFrom method allows you to pass a URL as the argument. Here is an example.

Assembly a = Assembly.LoadFrom(@"http://Wintellect.com/SomeAssembly.dll");

When you pass an Internet location, the CLR downloads the file, installs it into the user’s download cache, and loads the file from there. Note that you must be online or an exception will be thrown. However, if the file has been downloaded previously, and if Windows Internet Explorer has been set to work offline (see Internet Explorer’s Work Offline menu item in its File menu), the previously downloaded file will be used, and no exception will be thrown. You can also call UnsafeLoadFrom, which can load a web-downloaded assembly, bypassing some security checks.

💡重要提示:一台机器可能同时存在具有相同标识的多个程序集。由于 LoadFrom 会在内部调用 Load ,所以 CLR 有可能不是加载你指定的文件,而是加载一个不同的文件,从而造成非预期的行为。强烈建议每次生成程序集时都更改版本号,确保每个版本都有自己的唯一性标识,确保 LoadFrom 方法的行为符合预期。

Microsoft Visual Studio’s UI designers and other tools typically use Assembly’s LoadFile method. This method can load an assembly from any path and can be used to load an assembly with the same identity multiple times into a single AppDomain. This can happen as changes to an application’s UI are made in the designer/tool and the user rebuilds the assembly. When loading an assembly via LoadFile, the CLR will not resolve any dependencies automatically; your code must register with AppDomain’s AssemblyResolve event and have your event callback method explicitly load any dependent assemblies.

If you are building a tool that simply analyzes an assembly’s metadata via reflection (as discussed later in this chapter), and you want to ensure that none of the code contained inside the assembly executes, the best way for you to load an assembly is to use Assembly’s ReflectionOnlyLoadFrom method, or in some rarer cases, Assembly’s ReflectionOnlyLoad method. Here are the prototypes of both methods.

public class Assembly { 
 public static Assembly ReflectionOnlyLoadFrom(String assemblyFile); 
 public static Assembly ReflectionOnlyLoad(String assemblyString); 
 // Less commonly used overload of ReflectionOnlyLoad is not shown 
}

The ReflectionOnlyLoadFrom method will load the file specified by the path; the strongname identity of the file is not obtained, and the file is not searched for in the GAC or elsewhere. The ReflectionOnlyLoad method will search for the specified assembly looking in the GAC, application base directory, private paths, and codebases. However, unlike the Load method, the ReflectionOnlyLoad method does not apply versioning policies, so you will get the exact version that you specify. If you want to apply versioning policy yourself to an assembly identity, you can pass the string into AppDomain’s ApplyPolicy method.

When an assembly is loaded with ReflectionOnlyLoadFrom or ReflectionOnlyLoad, the CLR forbids any code in the assembly from executing; any attempt to execute code in an assembly loaded with either of these methods causes the CLR to throw an InvalidOperationException. These methods allow a tool to load an assembly that was delay-signed, would normally require security permissions that prevent it from loading, or was created for a different CPU architecture.

Frequently, when using reflection to analyze an assembly loaded with one of these two methods, the code will have to register a callback method with AppDomain’s ReflectionOnlyAssemblyResolve event to manually load any referenced assemblies (calling AppDomain’s ApplyPolicy method, if desired); the CLR doesn’t do it automatically for you. When the callback method is invoked, it must call Assembly’s ReflectionOnlyLoadFrom or ReflectionOnlyLoad method to explicitly load a referenced assembly and return a reference to this assembly.

💡注意:经常有人问到程序集卸载的问题。遗憾的是,CLR 不提供卸载单独程序集的能力。如果 CLR 允许这样做,那么一旦线程从某个方法返回至已卸载的一个程序集中的代码,如果允许应用程序以这样的一种方式崩溃,就和它的设计初衷背道而驰了。卸载程序集必须卸载包含它的整个 AppDomain。这方面的详情已在第 22 章 “CLR 寄宿和 AppDomain” 进行了讨论。

使用 ReflectionOnlyLoadFromReflectionOnlyLoad 方法加载的程序集表面上是可以卸载的。毕竟,这些程序集中的代码是不允许执行的。但 CLR 一样不允许执行的。但 CLR 一样不允许卸载用这两个方法加载的程序集。因为用这两个方法加载了程序集之后,仍然可以利用反射来创建对象,以便引用这些程序集中定义的元数据。如果卸载程序集,就必须通过某种方式使这些对象,以便引用这些程序集中定义的元数据。仍然可以利用反射来创建对象,以便引用这些程序集中定义的元数据。如果卸载程序集,就必须通过某种方式使这些对象失效。无论是实现的复杂性,还是执行速度,跟踪这些对象的状态都是得不偿失的。

Many applications consist of an EXE file that depends on many DLL files. When deploying this application, all the files must be deployed. However, there is a technique that you can use to deploy just a single EXE file. First, identify all the DLL files that your EXE file depends on that do not ship as part of the Microsoft .NET Framework itself. Then add these DLLs to your Visual Studio project. For each DLL file you add, display its properties and change its Build Action to Embedded Resource. This causes the C# compiler to embed the DLL file(s) into your EXE file, and you can deploy this one EXE file.

At run time, the CLR won’t be able to find the dependent DLL assemblies, which is a problem. To fix this, when your application initializes, register a callback method with the AppDomain’s ResolveAssembly event. The callback method’s code should look something like the following.

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(rn => 
rn.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);
 }
}

Now, the first time a thread calls a method that references a type in a dependent DLL file, the AssemblyResolve event will be raised and the preceding callback code will find the embedded DLL resource desired and load it by calling an overload of Assembly’s Load method that takes a Byte[] as an argument. Although I love the technique of embedding dependent DLLs inside another assembly, you should be aware that this does increase the memory used by your application at run time.

💡小结:JIT 编译器将方法的 IL 代码编译成本机代码时,会查看 IL 代码中引用了哪些类型。在运行时,JIT 编译器利用程序集的 TypeRef 和 AssemblyRef 元数据表来确定哪一个程序集定义了所引用的类型。在 AssemblyRef 元数据表的记录项中,包含了构成程序集强名称的各个部分。JIT 编译器尝试将与该标识匹配的程序集加载到 AppDomain 中 (如果还没有加载的话)。如果被加载的程序集是弱命名的,那么标识中就只包含程序集的名称 (不包含版本、语言文化及公钥标记信息)。在内部,CLR 使用 System.Reflection.Assembly 类的静态 Load 方法尝试加载这个程序集。该方法在 .NET Framework SDK 文档中是公开的,可调用它显式地将程序集加载到 AppDomain 中。该方法是 CLR 的与 Win32 LoadLibrary 函数等价的方法。在内部, Load 导致 CLR 向程序集应用一个版本绑定重定向策略,并在 GAC (全局程序集缓存) 中查找程序集。如果没找到,就接着去应用程序的基目录、私有路径子目录和 codebase 位置查找。如果调用 Load 时传递的是弱命名程序集, Load 就不会向程序集应用版本绑定重定向策略,CLR 也不会去 GAC 查找程序集。如果 Load 找到指定的程序集,会返回对代表已加载的那个程序集的一个 Assembly 对象的引用。如果 Load 没有找到指定程序集,会抛出一个 System.IO.FileNotFoundException 异常。调用 AssemblyLoadFrom 方法可以加载指定了路径名的程序集。在内部 LoadFrom 首先调用 System.Refection.AssemblyName 类的静态 GetAssemblyName 方法,该方法打开指定的文件,找到 AssemblyRef 元数据表的记录项,提取程序集标识信息,然后以一个 System.Reflection.AssemblyName 对象的形式返回这些信息 (文件同时会关闭)。随后, LoadFrom 方法在内部调用 AssemblyLoad 方法,将 AssemblyName 对象传给它。然后,CLR 应用版本绑定重定向策略,并在各个位置查找匹配的程序集。 Load 找到匹配程序集会加载它,并返回代表已加载程序集的 Assembly 对象; LoadFrom 方法将返回这个值。如果 Load 没有找到匹配的程序集, LoadFrom 会加载通过 LoadFrom 的实参传递的路径中的程序集。当然,如果已加载了具有相同标识的程序集, LoadFrom 方法就会直接返回代表已加载程序集的 Assembly 对象。Microsoft Visual Studio 的 UI 设计人员和其他工具一般用的是 AssemblyLoadFile 方法。这个方法可从任意路径加载程序集,而且可以将具有相同标识的程序集多次加载到一个 AppDomain 中。在设计器 / 工具中对应用程序的 UI 进行了修改,而且用户重新生成了程序集时,便有可能发生这种情况。通过 LoadFile 加载程序集时,CLR 不会自动解析任何依赖性问题;你的代码必须向 AppDomainAssemblyResolve 事件登记,并让事件回调方法显式地加载任何依赖的程序集。果你构建的一个工具只想通过反射 (本章稍后进行讨论) 来分析程序集的元数据,并希望确保程序集中的任何代码都不会执行,那么加载程序集的最佳方式就是使用 AssemblyReflectionOnlyLoadFrom 方法或者使用 AssemblyReflectionOnlyLoad 方法 (后者比较少见)。 ReflectionOnlyLoadFrom 方法加载由路径指定的文件;文件的强名称标识不会获取,也不会在 GAC 和其他位置搜索文件。 ReflectionOnlyLoad 方法会在 GAC、应用程序基目录、私有路径和 codebase 指定的位置搜索指定的程序集。但和 Load 方法不同的是, ReflectionOnlyLoad 方法不会应用版本控制策略,所以你指定的是哪个版本,获得的就是哪个版本。要自行向程序集标识应用版本控制策略,可将字符串传给 AppDomainApplyPolicy 方法。用 ReflectionOnlyLoadFromReflectionOnlyLoad 方法加载程序集时,CLR 禁止程序集中的任何代码执行;试图执行由这两个方法加载的程序集中的代码,会导致 CLR 抛出一个 InvalidOperationException 异常。这两个方法允许工具加载延迟签名的程序集,这种程序集正常情况下会因为安全权限不够而无法加载。另外,这种程序集也可能是为不同的 CPU 架构而创建的。利用反射来分析由这两个方法之一加载的程序集时,代码经常需要向 AppDomainReflectionOnlyAssemblyResolve 事件注册一个回调方法,以便手动加载任何引用的程序集 (如果必要,还需要调用 AppDomainApplyPolicy 方法);CLR 不会自动帮你做这个事情。回调方法被调用 (invoke) 时,它必须调用 (call) AssemblyReflectionOnlyLoadFromReflectionOnlyLoad 方法来显式加载引用的程序集,并返回对该程序集的引用。在运行时,CLR 会找不到依赖的 DLL 程序集。为了解决这个问题,当应用程序初始化时,可以向 AppDomainResolveAssembly 事件登记一个回调方法。

# Using Reflection to Build a Dynamically Extensible Application

As you know, metadata is stored in a bunch of tables. When you build an assembly or a module, the compiler that you’re using creates a type definition table, a field definition table, a method definition table, and so on. The System.Reflection namespace contains several types that allow you to write code that reflects over (or parses) these metadata tables. In effect, the types in this namespace offer an object model over the metadata contained in an assembly or a module.

Using these object model types, you can easily enumerate all of the types in a type definition metadata table. Then for each type, you can obtain its base type, the interfaces it implements, and the flags that are associated with the type. Additional types in the System.Reflection namespace allow you to query the type’s fields, methods, properties, and events by parsing the corresponding metadata tables. You can also discover any custom attributes (covered in Chapter 18, “Custom Attributes”) that have been applied to any of the metadata entities. There are even classes that let you determine referenced assemblies and methods that return the IL byte stream for a method. With all of this information, you could easily build a tool very similar to Microsoft’s ILDasm.exe.

💡注意:有的反射类型及其成员时专门由 CLR 编译器的开发人员使用的。应用程序的开发人员一般用不着。FCL 文档没有明确指出哪些类型和成员供编译器开发人员 (而非应用程序开发人员) 使用,但只要意识到有些反射类型及其成员不适合所有人使用,该文档时就会更清醒一些。

In reality, very few applications will have the need to use the reflection types. Reflection is typically used by class libraries that need to understand a type’s definition in order to provide some rich functionality. For example, the FCL’s serialization mechanism (discussed in Chapter 24, “Runtime Serialization”) uses reflection to determine what fields a type defines. The serialization formatter can then obtain the values of these fields and write them into a byte stream that is used for sending across the Internet, saving to a file, or copying to the clipboard. Similarly, Visual Studio’s designers use reflection to determine which properties should be shown to developers when laying out controls on their Web Forms or Windows Forms at design time.

Reflection is also used when an application needs to load a specific type from a specific assembly at run time to accomplish some task. For example, an application might ask the user to provide the name of an assembly and a type. The application could then explicitly load the assembly, construct an instance of the type, and call methods defined in the type. This usage is conceptually similar to calling Win32’s LoadLibrary and GetProcAddress functions. Binding to types and calling methods in this way is frequently referred to as late binding. (Early binding is when the types and methods used by an application are determined at compile time.)

💡小结:元数据是用一系列表来存储的。生成程序集或模块时,编译器会创建一个类型定义表、一个字段定义表、一个方法定义表以及其他表。利用 System.Reflection 命名空间中的其他类型,可以写代码来反射 (或者说” 解析 “) 这些元数据表。实际上,这个命名空间中类型为程序集或模块中包含的元数据提供了一个对象模型。利用对象模型中的类型,可以轻松枚举类型定义元数据表中的所有类型,而针对每个类型都可获取它的基类型、它实现的接口以及与类型关联的标志 (flag)。利用 System.Reflection 命名空间中的其他类型,还可解析对应的元数据表来查询类型的字段、方法、属性和事件。还可发现应用于任何元数据实体的定制特性 (详情参见第 18 章” 定制特性 “)。甚至有些类允许判断引用的程序集;还有一些方法能返回一个方法的 IL 字节流。事实上,只有极少数应用程序才需使用反射类型。如果类库需要理解类型的定义才能提供丰富的功能,就适合使用反射。在运行时,当应用程序需要从特定程序集中加载特定类型以执行特定任务时,也要使用反射。例如,应用程序可要求用户提供程序集和类别名。然后,应用程序可显式加载程序集,构造类型的实例,再调用类型中定义的方法。这种用法在概念上类似于调用 Win32 LoadLibraryGetProcAddress 函数。以这种用法在概念上类似于调用 Win32 LoadLibraryGetProAddress 函数。以这种方式绑定到类型并调用方法称为晚期绑定。(对应的,早期绑定是指在编译时就确定应用程序要使用的类型和方法。)

# Reflection Performance

Reflection is an extremely powerful mechanism because it allows you to discover and use types and members at run time that you did not know about at compile time. This power does come with two main drawbacks:

  • Reflection prevents type safety at compile time. Because reflection uses strings heavily, you lose type safety at compile time. For example, if you call Type.GetType("int"); to ask reflection to find a type called “int”, the code compiles but returns null at run time because the CLR knows the “int” type as “System.Int32”.

  • Reflection is slow. When using reflection, the names of types and their members are not known at compile time; you discover them at run time by using a string name to identify each type and member. This means that reflection is constantly performing string searches as the types in the System.Reflection namespace scan through an assembly’s metadata. Often, the string searches are case-insensitive comparisons, which can slow this down even more.

Invoking a member by using reflection will also hurt performance. When using reflection to invoke a method, you must first package the arguments into an array; internally, reflection must unpack these on to the thread’s stack. Also, the CLR must check that the arguments are of the correct data type before invoking a method. Finally, the CLR ensures that the caller has the proper security permission to access the member being invoked.

For all of these reasons, it’s best to avoid using reflection to access a field or invoke a method/ property. If you’re writing an application that will dynamically discover and construct type instances, you should take one of the following approaches:

  • Have the types derive from a base type that is known at compile time. At run time, construct an instance of the derived type, place the reference in a variable that is of the base type (by way of a cast), and call virtual methods defined by the base type.

  • Have the type implement an interface that is known at compile time. At run time, construct an instance of the type, place the reference in a variable that is of the interface type (by way of a cast), and call the methods defined by the interface.

I tend to prefer using the interface technique over the base type technique because the base type technique doesn’t allow the developer to choose the base type that works best in a particular situation. Although the base type technique works better in versioning scenarios, because you could always add a member to the base type and the derived types just inherit it; you can’t add a member to an interface without forcing all types that implement the interface to modify their code and recompile it.

When you use either of these two techniques, I strongly suggest that the interface or base type be defined in its own assembly. This will reduce versioning issues. For more information about how to do this, see the section titled “Designing an Application That Supports Add-Ins” later in this chapter.

# Discovering Types Defined in an Assembly

Reflection is frequently used to determine what types an assembly defines. The FCL offers many APIs to get this information. By far, the most commonly used API is Assembly’s ExportedTypes. property. Here is an example of code that loads an assembly and shows the names of all of the publicly exported types defined in it.

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) { 
 // Explicitly load an assembly in to this AppDomain 
 Assembly a = Assembly.Load(assemId); 
 // Execute this loop once for each Type 
 // publicly-exported from the loaded assembly 
 foreach (Type t in a.ExportedTypes) { 
 // Display the full name of the type 
 Console.WriteLine(t.FullName); 
 } 
 } 
}

# What Exactly Is a Type Object?

Notice that the previous code iterates over a sequence of System.Type objects. The System.Type type is your starting point for doing type and object manipulations. A System.Type object represents a type reference (as opposed to a type definition).

Recall that System.Object defines a public, nonvirtual instance method named GetType. When you call this method, the CLR determines the specified object’s type and returns a reference to its Type object. Because there is only one Type object per type in an AppDomain, you can use equality and inequality operators to see whether two objects are of the same type.

private static Boolean AreObjectsTheSameType(Object o1, Object o2) { 
 return o1.GetType() == o2.GetType(); 
}

In addition to calling Object’s GetType method, the FCL offers several more ways to obtain a Type object:

  • The System.Type type offers several overloaded versions of the static GetType method. All versions of this method take a String. The string must specify the full name of the type (including its namespace). Note that the primitive type names supported by the compiler (such as C#’s int, string, bool, and so on) aren’t allowed because these names mean nothing to the CLR. If the string is simply the name of a type, the method checks the calling assembly to see whether it defines a type of the specified name. If it does, a reference to the appropriate Type object is returned. If the calling assembly doesn’t define the specified type, the types defined by MSCorLib.dll are checked. If a type with a matching name still can’t be found, null is returned or a System. TypeLoadException is thrown, depending on which overload of the GetType method you called and what parameters you passed to it. The FCL documentation fully explains this method. You can pass an assembly-qualified type string, such as “System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089”, to GetType. In this case, GetType will look for the type in the specified assembly (loading the assembly if necessary).

  • The System.Type type offers a static ReflectionOnlyGetType method. This method behaves similarly to the GetType method mentioned in the previous bullet, except that the type is loaded so that it can be reflected over but cannot be executed.

  • The System.TypeInfo type offers the following instance members: DeclaredNestedTypes and GetDeclaredNestedType.

  • The System.Reflection.Assembly type offers the following instance members: GetType, DefinedTypes, and ExportedTypes.

💡注意:构造传给反射方法的字符串时,要使用类型名称或限定了程序集的类型名称。Microsoft 为这些名称定义了巴克斯 —— 诺尔范式 (Backus-MaurForm, BNF) 语法。使用反射时,了解这种语法对你很有帮助,尤其是在处理嵌套类型、泛型类型、泛型类型、引用参数或者数组的时候。要想了解完整语法,请参考文档或者 Google “BNF 类型名称”。还可参考 TypeTypeInfoMakeArrayType,MakeByRefType,MakeGenericTypeMakePointerType 方法。

Many programming languages also offer an operator that allows you to obtain a Type object from a type name that is known at compile time. When possible, you should use this operator to obtain a reference to a Type instead of using any of the methods in the preceding list, because the operator generally produces faster code. In C#, the operator is called typeof, and you use this operator typically to compare late-bound type information with early-bound (known at compile time) type information. The following code demonstrates an example of its use.

private static void SomeMethod(Object o) { 
 // GetType returns the type of the object at runtime (late-bound) 
 // typeof returns the type of the specified class (early-bound) 
 if (o.GetType() == typeof(FileInfo)) { ... } 
 if (o.GetType() == typeof(DirectoryInfo)) { ... } 
}

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

As mentioned earlier, a Type object represents a type reference that is a lightweight object. If you want to learn more about the type itself, then you must acquire a TypeInfo object, which represents a type definition. You can convert a Type object to a TypeInfo object by calling System.Reflection.IntrospectionExtensions’ GetTypeInfo extension method.

Type typeReference = ...; // For example: o.GetType() or typeof(Object)
TypeInfo typeDefinition = typeReference.GetTypeInfo();

And, although less useful, you can convert a TypeInfo object to a Type object by calling TypeInfo’s AsType method

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

Obtaining a TypeInfo object forces the CLR to resolve the type by ensuring that the assembly that defines the type is loaded. This can be an expensive operation that can be avoided if all you need are type references (Type objects). However, after you have a TypeInfo object, you can query many of the type’s properties to learn more about it. Most of the properties, such as IsPublic, IsSealed, IsAbstract, IsClass, IsValueType, and so on, indicate flags associated with the type. Other properties, such as Assembly, AssemblyQualifiedName, FullName, Module, and so on, return the name of the type’s defining assembly or module and the full name of the type. You can also query the BaseType property to obtain a reference to the type’s base type, and a slew of members will give you even more information about the type. The FCL documentation describes all of the methods and properties that TypeInfo exposes.

# Building a Hierarchy of Exception-Derived Types

The following code uses many of the concepts discussed already in this chapter to load a bunch of assemblies into the AppDomain and display all of the classes that are ultimately derived from System. Exception. By the way, this is the program I wrote to build the exception hierarchy displayed in the “FCL-Defined Exception Classes” section in Chapter 20, “Exceptions and State Management.”

public static void Go() {
 // Explicitly load the assemblies that we want to reflect over
 LoadAssemblies();
 // Filter & sort all the types
 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();
 // Build the inheritance hierarchy tree and show it
 Console.WriteLine(WalkInheritanceHierarchy(new StringBuilder(), 0, typeof(Exception), 
allTypes));
}
private static StringBuilder WalkInheritanceHierarchy(
 StringBuilder sb, Int32 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}",
 "System.DirectoryServices, PublicKeyToken={1}",
 "System.Drawing, PublicKeyToken={1}",
 "System.Drawing.Design, PublicKeyToken={1}",
 "System.Management, PublicKeyToken={1}",
 "System.Messaging, PublicKeyToken={1}",
 "System.Runtime.Remoting, PublicKeyToken={0}",
 "System.Security, PublicKeyToken={1}",
 "System.ServiceProcess, PublicKeyToken={1}",
 "System.Web, PublicKeyToken={1}",
 "System.Web.RegularExpressions, PublicKeyToken={1}",
 "System.Web.Services, PublicKeyToken={1}",
 "System.Xml, PublicKeyToken={0}",
 };
 String EcmaPublicKeyToken = "b77a5c561934e089";
 String MSPublicKeyToken = "b03f5f7f11d50a3a";
 // Get the version of the assembly containing System.Object
 // We'll assume the same version for all the other assemblies
 Version version = typeof(System.Object).Assembly.GetName().Version;
 // Explicitly load the assemblies that we want to reflect over
 foreach (String a in assemblies) {
 String AssemblyIdentity = 
 String.Format(a, EcmaPublicKeyToken, MSPublicKeyToken) +
 ", Culture=neutral, Version=" + version;
 Assembly.Load(AssemblyIdentity);
 }
}

# Constructing an Instance of a Type

After you have a reference to a Type-derived object, you might want to construct an instance of this type. The FCL offers several mechanisms to accomplish this:

  • System.Activator’s CreateInstance methods The Activator class offers several overloads of its static CreateInstance method. When you call this method, you can pass either a reference to a Type object or a String that identifies the type of object you want to create. The versions that take a type are simpler. You get to pass a set of arguments for the type’s constructor, and the method returns a reference to the new object.

    The versions of this method in which you specify the desired type by using a string are a bit more complex. First, you must also specify a string identifying the assembly that defines the type. Second, these methods allow you to construct a remote object if you have remoting options configured properly. Third, these versions don’t return a reference to the new object. Instead, they return a System.Runtime.Remoting.ObjectHandle (which is derived from System.MarshalByRefObject).

    An ObjectHandle is a type that allows an object created in one AppDomain to be passed around to other AppDomains without forcing the object to materialize. When you’re ready to materialize the object, you call ObjectHandle’s Unwrap method. This method loads the assembly that defines the type being materialized in the AppDomain where Unwrap is called. If the object is being marshaled by reference, the proxy type and object are created. If the object is being marshaled by value, the copy is deserialized.

  • System.Activator’s CreateInstanceFrom methods The Activator class also offers a set of static CreateInstanceFrom methods. These methods behave just as the CreateInstance method, except that you must always specify the type and its assembly via string parameters. The assembly is loaded into the calling AppDomain by using Assembly’s LoadFrom method (instead of Load). Because none of these methods takes a Type parameter, all of the CreateInstanceFrom methods return a reference to an ObjectHandle, which must be unwrapped.

  • System.AppDomain’s methods The AppDomain type offers four instance methods (each with several overloads) that construct an instance of a type: CreateInstance, CreateInstanceAndUnwrap, CreateInstanceFrom, and CreateInstanceFromAndUnwrap. These methods work just as Activator’s methods except that these methods are instance methods, allowing you to specify which AppDomain the object should be constructed in. The methods that end with Unwrap exist for convenience so that you don’t have to make an additional method call.

  • System.Reflection.ConstructorInfo’s Invoke instance method Using a reference to a TypeInfo object, you can bind to a particular constructor and obtain a reference to the constructor’s ConstructorInfo object. Then you can use the reference to the ConstructorInfo object to call its Invoke method. The type is always created in the calling AppDomain, and a reference to the new object is returned. I’ll also discuss this method in more detail later in this chapter.

💡注意:CLR 不要求值类型定义任何构造器。但这会造成一个问题,因为上述列表中的所有机制都要求调用构造器来构造对象。然而, ActivatorCreateInstance 方法允许在不调用构造器的情况下创建值类型的实例。要在不调用构造器的情况下创建值类型的实例,必须调用 CreateInstance 方法获取单个 Type 参数的版本或者获取 TypeBoolean 参数的版本。

The mechanisms just listed allow you to create an object for all types except for arrays (System. Array-derived types) and delegates (System.MulticastDelegate-derived types). To create an array, you should call Array’s static CreateInstance method (several overloaded versions exist). The first parameter to all versions of CreateInstance is a reference to the Type of elements you want in the array. CreateInstance’s other parameters allow you to specify various combinations of dimensions and bounds. To create a delegate, you should call MethodInfo’s CreateDelegate method. The first parameter to all versions of CreateDelegate is a reference to the Type of delegate you want to create. CreateDelegate’s other parameter allows you to specify which object should be passed as the this parameter when calling an instance method.

To construct an instance of a generic type, first get a reference to the open type, and then call Type’s MakeGenericType method, passing in an array of types that you want to use as the type arguments. Then, take the returned Type object and pass it into one of the various methods previously listed. Here is an example.

using System; 
using System.Reflection; 
internal sealed class Dictionary<TKey, TValue> { } 
public static class Program { 
 public static void Main() { 
 // Get a reference to the generic type's type object 
 Type openType = typeof(Dictionary<,>); 
 // Close the generic type by using TKey=String, TValue=Int32 
 Type closedType = openType.MakeGenericType(typeof(String), typeof(Int32)); 
 // Construct an instance of the closed type 
 Object o = Activator.CreateInstance(closedType); 
 // Prove it worked 
 Console.WriteLine(o.GetType()); 
 } 
}

If you compile the preceding code and run it, you get the following output.

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

# Designing an Application That Supports Add-Ins

When you’re building extensible applications, interfaces should be the centerpiece. You could use a base class instead of an interface, but in general, an interface is preferred because it allows add-in developers to choose their own base class. Suppose, for example, that you’re writing an application and you want others to be able to create types that your application can load and use seamlessly.

Here’s the way to design this application:

  • Create a Host SDK assembly that defines an interface whose methods are used as the communication mechanism between the host application and the add-in components. When defining the parameters and return types for the interface methods, try to use other interfaces or types defined in MSCorLib.dll. If you want to pass and return your own data types, define them in this Host SDK assembly, too. After you settle on your interface definitions, give this assembly a strong name (discussed in Chapter 3), and then package and deploy it to your partners and users. Once published, you should really avoid making any kind of breaking changes to the types in this assembly. For example, do not change the interface in any way. However, if you define any data types, it is OK to add new members. If you make any modifications to the assembly, you’ll probably want to deploy it with a publisher policy file (also discussed in Chapter 3).

💡注意:之所以能使用 MSCorLib.dll 中定义的类型,是因为 CLR 总是加载与 CLR 本身的版本匹配的那个版本的 MSCorLib.dll。此外,一个 CLR 实例只会加载一个版本的 MSCorLib.dll 。换言之,永远不会出现多个不同版本的 MSCorLib.dll 都加载的情况 (详见第 3 章)。最后结果就是,绝不会出现类型版本不匹配的情况。这还有助于减少应用程序对内存的需求。

  • The add-in developers will, of course, define their own types in their own Add-In assembly. Their Add-In assembly will reference the types in your Host SDK assembly. The add-in developers are able to put out a new version of their assembly as often as they’d like, and the host application will be able to consume the add-in types without any problem whatsoever.

  • Create a separate Host Application assembly containing your application’s types. This assembly will obviously reference the Host SDK assembly and use the types defined in it. Feel free to modify the code in the Host Application assembly to your heart’s desire. Because the add-in developers don’t reference the Host Application assembly, you can put out a new version of it every hour if you want to and not affect any of the add-in developers.

This section contains some very important information. When using types across assemblies, you need to be concerned with assembly-versioning issues. Take your time to architect this cleanly by isolating the types that you use for communication across assembly boundaries into their own assembly. Avoid mutating or changing these type definitions. However, if you really need to modify the type definitions, make sure that you change the assembly’s version number and create a publisher policy file for the new version.

I’ll now walk through a very simple scenario that puts all of this together. First, here is the code for the HostSDK.dll assembly.

using System; 
namespace Wintellect.HostSDK { 
 public interface IAddIn { 
 String DoSomething(Int32 x); 
 } 
}

Second, here is the code for an AddInTypes.dll assembly defining two public types that implement the HostSDK’s interface. To build this assembly, the HostSDK.dll assembly must be referenced.

using System; 
using Wintellect.HostSDK; 
public sealed class AddIn_A : IAddIn { 
 public AddIn_A() { 
 } 
 public String DoSomething(Int32 x) { 
 return "AddIn_A: " + x.ToString(); 
 } 
} 
public sealed class AddIn_B : IAddIn { 
 public AddIn_B() { 
 } 
 public String DoSomething(Int32 x) { 
 return "AddIn_B: " + (x * 2).ToString(); 
 } 
}

Third, here is the code for a simple Host.exe assembly (a console application). To build this assembly, the HostSDK.dll assembly must be referenced. To discover usable add-in types, this host code assumes that the types are defined in assemblies ending with a .dll file extension and that these assemblies are deployed into the same directory as the host’s EXE file. Microsoft’s Managed Extensibility Framework (MEF) is built on top of the various mechanisms that I show here, and it also offers add-in registration and discovery mechanisms. I urge you to check MEF out if you are building a dynamically extensible application, because it can simplify some of the material in this chapter.

using System; 
using System.IO; 
using System.Reflection; 
using System.Collections.Generic; 
using Wintellect.HostSDK; 
public static class Program { 
 public static void Main() {
 // Find the directory that contains the Host exe
 String AddInDir = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);
 // Assume AddIn assemblies are in same directory as host's EXE file
 var AddInAssemblies = Directory.EnumerateFiles(AddInDir, "*.dll");
 // Create a collection of Add-In Types usable by the host
 var AddInTypes =
 from file in AddInAssemblies
 let assembly = Assembly.Load(file)
 from t in assembly.ExportedTypes // Publicly-exported types
 // Type is usable if it is a class that implements IAddIn 
 where t.IsClass && typeof(IAddIn).GetTypeInfo().IsAssignableFrom(t.GetTypeInfo())
 select t;
 // Initialization complete: the host has discovered the usable Add-Ins
 // Here's how the host can construct Add-In objects and use them
 foreach (Type t in AddInTypes) {
 IAddIn ai = (IAddIn) Activator.CreateInstance(t);
 Console.WriteLine(ai.DoSomething(5));
 }
 }
}

The simple host/add-in scenario just shown doesn’t use AppDomains. However, in a real-life scenario, you will likely create each add-in in its own AppDomain with its own security and configuration settings. And of course, each AppDomain could be unloaded if you wanted to remove an add-in from memory. To communicate across the AppDomain boundary, you’d either tell the add-in developers to derive their add-in types from MarshalByRefObject or, more likely, have the host application define its own internal type that is derived from MarshalByRefObject. As each AppDomain is created, the host would create an instance of its own MarshalByRefObject-derived type in the new AppDomain. The host’s code (in the default AppDomain) would communicate with its own type (in the other AppDomains) to have it load add-in assemblies and create and use instances of the add-in types.

💡小结:反射是相当强大的机制,允许在运行发现并使用编译时还不了解的类型及其成员。但是,它也有下面两个缺点。反射造成编译时无法保证类型安全性。由于反射严重依赖字符串,所以会丧失编译时的类型安全性。反射速度慢。使用反射时,类型及其成员的名称在编译时未知;你要用字符串名称标识每个类型及其成员,然后在运行时发现它们。也就是说,使用 System.Reflection 命名空间中的类型扫描程序集的元数据时,反射机制会不停地执行字符串搜索。通常,字符串搜索执行的是不区分大小写的比较,这会进一步影响速度。使用反射调用成员也会影响性能。用反射调用方法时,首先必须将实参打包 (pack) 成数组;在内部,反射必须将这些实参解包 (unpack) 到线程栈上。此外,在调用方法前,CLR 必须检查实参具有正确的数据类型。最后,CLR 必须确保调用者有正确的安全权限来访问被调用的成员。基于上述所有原因,最好避免利用反射来访问字段或调用方法 / 属性。应该利用一下两种技术之一开发应用程序来动态发现和构造类型实例。让类型从编译时已知的基类型派生。在运行时构造派生类型的实例,将对它的引用方法放到基类型的变量中 (利用转型),再调用基类型定义的虚方法。让类型实现编译时已知的接口。在运行时构造类型的实例,将对它的引用放到接口类型的变量中 (利用转型),再调用接口定义的方法。使用这两种技术时,强烈建议接口或基类型在它们自己的程序集中定义,这有助于缓解版本控制问题。 System.Type 类型是执行类型和对象操作的起点。 System.Type 对象代表一个类型引用 (而不是类型定义)。 System.Object 定义了公共非虚实例方法 GetType 。调用这个方法时,CLR 会判断指定对象的类型,并返回对该类型的 Type 对象的引用。由于在一个 AppDomain 中,每个类型只有一个 Type 对象,所以可以使用相等和不等操作符来判断两个对象是不是相同的类型。除了调用 ObjectGetType 方法, FCL 还提供了获得 Type 对象的其他几种方式。 System.Type 类型提供了静态 GetType 方法的几个重载版本。所有版本都接收一个 String 参数。字符串必须指定类型的全名 (包括它的命名空间)。注意不允许使用编译器支持的基元类型 (比如 C# 的 intstringbool 等),这些名称对于 CLR 没有任何意义。如果传递的只是一个类型名称,方法将检查调用程序集,看它是否定义了指定名称的类型。如果是,就返回对恰当 Type 对象的引用。如果调用程序集没有定义指定的类型,就检查 MSCorLib.dll 定义的类型。如果还是没有找到,就返回 null 或抛出 System.TypeLoadException (取决于调用的是 GetType 方法的哪个重载,以及传递的是什么参数)。 System.Type 类型提供了静态 ReflectionOnlyGetType 方法。该方法与上一条提到的 GetType 方法在行为上相似,只是类型会以 “仅反射” 的方式加载,不能执行。 System.TypeInfo 类型提供了实例成员 DeclaredNestedTypesGetDeclaredNestedTypeSystem.Reflection.Assembly 类型提供了实例成员 GetTypeDefinedTypesExportedTypes 。许多编程语言都允许使用一个操作符并根据编译时已知的类型名称来获得 Type 对象。尽量用这个操作符获得 Type 引用,而不要使用上述列表中的任何方法,因为操作符生成的代码通常更快。C# 的这个操作符称为 typeof ,通常用它将晚期绑定的类型信息与早期绑定 (编译时已知) 的类型信息进行比较。 Type 对象是轻量级的对象引用。要更多地了解类型本身,必须获取一个 TypeInfo 对象,后者才代表类型定义。可调用 System.Reflection.IntrospectionExtensionsGetTypeInfo 扩展方法将 Type 对象转换成 TypeInfo 对象。另外,虽然作用不大,但还可调用 TypeInfoAsType 方法将 TypeInfo 对象转换成 Type 对象。获取 TypeInfo 对象会强迫 CLR 确保已加载类型的定义程序集,从而对类型进行解析。这个操作可能代价高昂。如果只需要类型引用 ( Type 对象),就应避免这个操作。但一旦获得了 TypeInfo 对象,就可查询类型的许多属性进一步了解它。大多数属性,比如 IsPublicIsSealedIsAbstractIsClassIsValueType 等,都指明了与类型关联的标志。另一些属性,比如 AssemblyAssemblyQualifiedName , FullNameModule 等,则返回定义该类型的程序集或模块的名称以及类型的全名。还可查询 BaseType 属性来获取对类型的基类型的引用。除此之外,还有许多方法能提供关于类型的更多信息。获得对 Type 派生对象的引用之后,就可以构造该类型的实例了。FCL 提供了以下几个机制。 Activator 类提供了静态 CreateInstance 方法的几个重载版本。调用方法时既可传递一个 Type 对象引用,也可传递标识了类型的 String 。直接获取类型对象的几个版本较为简单。你要为类型的构造器传递一组实参,方法返回对新对象的引用。用字符串来指定类型的几个版本则稍微复杂一些。首先必须指定另一个字符串来标识定义了类型的程序集。其次,如果正确匹配了远程访问 (remoting) 选项,这些方法还允许构造远程对象。第三,这次版本返回的不是对新对象的引用,而是一个 System.Runtime.Remoting.ObjectHandle 对象 (从 System.MarshalByRefObject 派生)。 ObjectHandle 类型允许将一个 AppDomain 中创建的对象传至其他 AppDomain,期间不强迫对象具体化 (materialize)。准备好具体化这个对象时,请调用 ObjectHandleUnwrap 方法。在一个 AppDomain 中调用该方法时,它将定义了要具体化的类型的程序集加载到这个 AppDomain 中。如果对象按引用封送,会创建代理类型和对象。如果对象按值封送,对象的副本会被反序列化。 Activator 类还提供了一组静态 CreateInstanceFrom 方法。它们与 CreateInstance 的行为相似,只是必须通过字符串参数来指定类型及其程序集。程序集用 AssemblyLoadFrom (而非 Load) 方法加载到调用 AppDomain 中。由于都不接受 Type 参数,所以返回的都会一个 ObjectHandle 对象引用,必须调用 ObjectHandleUnwrap 方法进行具体化。 AppDomain 类型提供了 4 个用于构造类型实例的实例方法 (每个都有几个重载版本),包括 CreateInstanceCreateInstanceAndUnwrapCreateInstanceFromCreateInstanceFromAndUnwrap 。这些方法的行为和 Activator 类的方法相似,区别在于它们都是实例方法,允许指定在哪个 AppDomain 中构造对象。另外,带 Unwrap 后缀的方法还能简化操作,不必执行额外的方法调用。使用一个 Type 对象引用,可以绑定到一个特定的构造器,并获取对构造器的 ConstructorInfo 对象的引用。然后,可利用 ConstructorInfo 对象引用来调用它的 Invoke 方法。类型总是在调用 AppDomain 中创建,返回的是对新对象的引用。利用前面列出的机制,可为出数组 ( System.Array 派生类型) 和委托 ( System.MulticastDelegate 派生类型) 之外的所有类型创建对象。创建数组需要调用 Array 的静态 CreateInstance 方法 (有几个重载的版本)。所有版本的 CreateInstance 方法获取的第一个参数都是对数组元数 Type 的引用。 CreateInstance 的其他参数允许指定数组维数和上下限的各种组合。创建委托则要调用 Delegate 的静态 CreateDelegate 方法。所有版本的 CreateDelegate 方法获取的第一个参数都是对委托 Type 的引用。 CreateDelegate 方法的其他参数允许指定在调用实例方法时应将哪个对象作为 this 参数传递。构建可扩展应用程序时,接口是中心。可用基类代替接口,但接口通常是首选的,因为它允许加载项开发人员选择他们自己的基类。跨程序集使用类型时,需要关注程序集的版本控制问题。要花一些时间精心建构,将跨程序集通信的类型隔离到它们自己的程序集中。要避免以后更改这些类型的定义。但是,如果真的要修改类型定义,一定要修改程序集的版本号,并为新版本的程序集创建发布者策略文件。为了跨 AppDomain 边界通信,可告诉加载项开发人员从 MarshalByRefObject 派生出他们自己的加载类型。但另一个更常见的办法是让宿主应用程序定义自己的、从 MarshalByRefObject 派生的内部类型。每个 AppDomain 创建好后,宿主要在新 AppDomain 中创建它自己的 MarshalByRefObject 派生类型实例。宿主的代码 (位于默认 AppDomain 中) 将与它自己的类型 (位于其他 AppDomain 中) 通信,让后者载入加载项程序集,并创建和使用加载的类型的实例。

# Using Reflection to Discover a Type’s Members

So far, this chapter has focused on the parts of reflection—assembly loading, type discovery, and object construction—necessary to build a dynamically extensible application. In order to have good performance and compile-time type safety, you want to avoid using reflection as much as possible. In the dynamically extensible application scenario, after an object is constructed, the host code typically casts the object to an interface type or a base class that is known at compile time; this allows the object’s members to be accessed in a high-performance and compile-time type-safe way.

In the remainder of this chapter, I’m going to focus on some other aspects of reflection that you can use to discover and then invoke a type’s members. The ability to discover and invoke a type’s members is typically used to create developer tools and utilities that analyze an assembly by looking for certain programming patterns or uses of certain members. Examples of tools/utilities that do this are ILDasm.exe, FxCopCmd.exe, and Visual Studio’s Windows Forms, Windows Presentation Foundation, and Web Forms designers. In addition, some class libraries use the ability to discover and invoke a type’s members in order to offer rich functionality as a convenience to developers. Examples of class libraries that do so are serialization/deserialization and simple data binding.

# Discovering a Type’s Members

Fields, constructors, methods, properties, events, and nested types can all be defined as members within a type. The FCL contains a type called System.Reflection.MemberInfo. This class is an abstract base class that encapsulates a bunch of properties common to all type members. Derived from MemberInfo are a bunch of classes; each class encapsulates some more properties related to a specific type member. Figure 23-1 shows the hierarchy of these types.

image-20221202202124953

The following program demonstrates how to query a type’s members and display some information about them. This code processes all of the public types defined in all assemblies loaded in the calling AppDomain. For each type, the DeclaredMembers property is called and returns a collection of MemberInfo-derived objects; each object refers to a single member defined within the type. Then, for each member, its kind (field, constructor, method, property, etc.) and its string value (obtained by calling ToString) is shown.

using System; 
using System.Reflection; 
public static class Program { 
 public static void Main() { 
 // Loop through all assemblies loaded in this AppDomain 
 Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); 
 foreach (Assembly a in assemblies) { 
 Show(0, "Assembly: {0}", a); 
 // Find Types in the assembly
 foreach (Type t in a.ExportedTypes) {
 Show(1, "Type: {0}", t);
 // Discover the type's members
 foreach (MemberInfo 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 = "ConstructoInfo";
 if (mi is PropertyInfo) typeName = "PropertyInfo";
 if (mi is EventInfo) typeName = "EventInfo";
 Show(2, "{0}: {1}", typeName, mi);
 }
 } } 
 } 
 private static void Show(Int32 indent, String format, params Object[] args) { 
 Console.WriteLine(new String(' ', 3 * indent) + format, args); 
 } 
}

When you compile and run this code, a ton of output is produced. Here is a small sampling of what it looks like.

Assembly: mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
 Type: System.Object
 MethodInfo: System.String ToString()
 MethodInfo: Boolean Equals(System.Object)
 MethodInfo: Boolean Equals(System.Object, System.Object)
 MethodInfo: Boolean ReferenceEquals(System.Object, System.Object)
 MethodInfo: Int32 GetHashCode()
 MethodInfo: System.Type GetType()
 MethodInfo: Void Finalize()
 MethodInfo: System.Object MemberwiseClone()
 MethodInfo: Void FieldSetter(System.String, System.String, System.Object)
 MethodInfo: Void FieldGetter(System.String, System.String, System.Object ByRef)
 MethodInfo: System.Reflection.FieldInfo GetFieldInfo(System.String, System.String)
 ConstructoInfo: Void .ctor()
 Type: System.Collections.Generic.IComparer`1[T]
 MethodInfo: Int32 Compare(T, T)
 Type: System.Collections.IEnumerator
 MethodInfo: Boolean MoveNext()
 MethodInfo: System.Object get_Current()
 MethodInfo: Void Reset()
 PropertyInfo: System.Object Current
 Type: System.IDisposable
 MethodInfo: Void Dispose()
 Type: System.Collections.Generic.IEnumerator`1[T]
 MethodInfo: T get_Current()
 PropertyInfo: T Current
 Type: System.ArraySegment`1[T]
 MethodInfo: T[] get_Array()
 MethodInfo: Int32 get_Offset()
 MethodInfo: Int32 get_Count()
 MethodInfo: Int32 GetHashCode()
 MethodInfo: Boolean Equals(System.Object)
 MethodInfo: Boolean Equals(System.ArraySegment`1[T])
 MethodInfo: Boolean op_Equality(System.ArraySegment`1[T], System.ArraySegment`1[T])
 MethodInfo: Boolean op_Inequality(System.ArraySegment`1[T], System.ArraySegment`1[T])
 ConstructoInfo: Void .ctor(T[])
 ConstructoInfo: Void .ctor(T[], Int32, Int32)
 PropertyInfo: T[] Array
 PropertyInfo: Int32 Offset
 PropertyInfo: Int32 Count
 FieldInfo: T[] _array
 FieldInfo: Int32 _offset

Because MemberInfo is the root of the member hierarchy, it makes sense for us to discuss it a bit more. Table 23-1 shows several read-only properties and methods offered by the MemberInfo class. These properties and methods are common to all members of a type. Don’t forget that System. TypeInfo is derived from MemberInfo, and therefore, TypeInfo also offers all of the properties shown in Table 23-1.

image-20221202202228344

Each element of the collection returned by querying DeclaredMembers is a reference to one of the concrete types in the hierarchy. Although TypeInfo’s DeclaredMembers property returns all of the type’s members, TypeInfo also offers methods that return specific member types for a specified string name. For example, TypeInfo offers GetDeclaredNestedType, GetDeclaredField, GetDeclaredMethod, GetDeclaredProperty, and GetDeclaredEvent. These methods all return a reference to a TypeInfo object, FieldInfo object, MethodInfo object, PropertyInfo object, or EventInfo object, respectively. There is also a GetDeclaredMethods method that returns a collection of MethodInfo objects describing the methods matching the specified string name.

Figure 23-2 summarizes the types used by an application to walk reflection’s object model. From an AppDomain, you can discover the assemblies loaded into it. From an assembly, you can discover the modules that make it up. From an assembly or a module, you can discover the types that it defines. From a type, you can discover its nested types, fields, constructors, methods, properties, and events. Namespaces are not part of this hierarchy because they are simply syntactical gatherings of types. If you want to list all of the namespaces defined in an assembly, you need to enumerate all of the types in this assembly and take a look at their Namespace property.

From a type, it is also possible to discover the interfaces it implements. And from a constructor, method, property accessor method, or event add/remove method, you can call the GetParameters method to obtain an array of ParameterInfo objects, which tells you the types of the member’s parameters. You can also query the read-only ReturnParameter property to get a ParameterInfo object for detailed information about a member’s return type. For a generic type or method, you can call the GetGenericArguments method to get the set of type parameters. Finally, for any of these items, you can query the CustomAttributes property to obtain the set of custom attributes applied to them.

image-20221202202318687

# Invoking a Type’s Members

Now that you know how to discover the members defined by a type, you may want to invoke one of these members. What invoke means depends on the kind of member being invoked. Table 23-2 shows which method to call for each kind of member to invoke that member.

image-20221202202419107

The PropertyInfo type represents metadata information about a property (as discussed in Chapter 10, “Properties”); that is, PropertyInfo offers CanRead, CanWrite, and PropertyType read-only properties. These properties indicate whether a property is readable or writeable and what data type the property is. PropertyInfo also has read-only GetMethod and SetMethod properties, which return MethodInfo objects representing the methods that get and set a property’s value.

PropertyInfo’s GetValue and SetValue methods exist for convenience; internally, they invoke the appropriate MethodInfo object. To support parameterful properties (C# indexers), the GetValue and SetValue methods offer an index parameter of Object[] type.

The EventInfo type represents metadata information about an event (as discussed in Chapter 11, “Events”). The EventInfo type offers a read-only EventHandlerType property that returns the Type of the event’s underlying delegate. The EventInfo type also has read-only AddMethod and RemoveMethod properties, which return the MethodInfo objects corresponding to the methods that add or remove a delegate to/from the event. To add or remove a delegate, you can invoke these MethodInfo objects, or you can call EventInfo’s more convenient AddEventHandler and RemoveEventHandler methods.

The following sample application demonstrates the various ways to use reflection to access a type’s members. The SomeType class represents a type that has various members: a private field (m_someField), a public constructor (SomeType) that takes an Int32 argument passed by reference, a public method (ToString), a public property (SomeProp), and a public event (SomeEvent). Having defined the SomeType type, I offer three different methods that use reflection to access SomeType’s members. Each method uses reflection in a different way to accomplish the same thing.

  • The BindToMemberThenInvokeTheMember method demonstrates how to bind to a member and invoke it later.

  • The BindToMemberCreateDelegateToMemberThenInvokeTheMember method demonstrates how to bind to an object or member, and then it creates a delegate that refers to that object or member. Calling through the delegate is very fast, and this technique yields faster performance if you intend to invoke the same member on the same object multiple times.

  • The UseDynamicToBindAndInvokeTheMember method demonstrates how to use C# dynamic primitive type (discussed at the end of Chapter 5, “Primitive, Reference, and Value Types”) to simplify the syntax for accessing members. In addition, this technique can give reasonably good performance if you intend to invoke the same member on different objects that are all of the same type because the binding will happen once per type and be cached so that it can be invoked multiple times quickly. You can also use this technique to invoke a member on objects of different types.

using System; 
using System.Reflection; 
using Microsoft.CSharp.RuntimeBinder;
using System.Linq;
// This class is used to demonstrate reflection 
// It has a field, constructor, method, property, and an event 
internal sealed class SomeType { 
 private Int32 m_someField; 
 public SomeType(ref Int32 x) { x *= 2; } 
 public override String ToString() { return m_someField.ToString(); } 
 public Int32 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();} 
} 
public static class Program { 
 public static void Main() { 
 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");
 // Construct an instance
 Type ctorArgument = Type.GetType("System.Int32&"); // or typeof(Int32).MakeByRefType();
 ConstructorInfo ctor = t.GetTypeInfo().DeclaredConstructors.First(
 c => c.GetParameters()[0].ParameterType == ctorArgument);
 Object[] args = new Object[] { 12 }; // Constructor arguments
 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]);
 // Read and write to a field
 FieldInfo fi = obj.GetType().GetTypeInfo().GetDeclaredField("m_someField");
 fi.SetValue(obj, 33);
 Console.WriteLine("someField: " + fi.GetValue(obj));
 // Call a method
 MethodInfo mi = obj.GetType().GetTypeInfo().GetDeclaredMethod("ToString");
 String s = (String)mi.Invoke(obj, null);
 Console.WriteLine("ToString: " + s);
 // Read and write a property
 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));
 // Add and remove a delegate from the event
 EventInfo ei = obj.GetType().GetTypeInfo().GetDeclaredEvent("SomeEvent");
 EventHandler eh = new EventHandler(EventCallback); // See ei.EventHandlerType
 ei.AddEventHandler(obj, eh);
 ei.RemoveEventHandler(obj, eh);
 }
 // Callback method added to the event
 private static void EventCallback(Object sender, EventArgs e) { }
 private static void BindToMemberCreateDelegateToMemberThenInvokeTheMember(Type t) {
 Console.WriteLine("BindToMemberCreateDelegateToMemberThenInvokeTheMember");
 // Construct an instance (You can't create a delegate to a constructor)
 Object[] args = new Object[] { 12 }; // Constructor arguments
 Console.WriteLine("x before constructor called: " + args[0]);
 Object obj = Activator.CreateInstance(t, args);
 Console.WriteLine("Type: " + obj.GetType().ToString());
 Console.WriteLine("x after constructor returns: " + args[0]);
 // NOTE: You can't create a delegate to a field
 // Call a method
 MethodInfo mi = obj.GetType().GetTypeInfo().GetDeclaredMethod("ToString");
 var toString = mi.CreateDelegate<Func<String>>(obj);
 String s = toString();
 Console.WriteLine("ToString: " + s);
 // Read and write a property
 PropertyInfo pi = obj.GetType().GetTypeInfo().GetDeclaredProperty("SomeProp");
 var setSomeProp = pi.SetMethod.CreateDelegate<Action<Int32>>(obj);
 try {
 setSomeProp(0);
 }
 catch (ArgumentOutOfRangeException) {
 Console.WriteLine("Property set catch.");
 }
 setSomeProp(2);
 var getSomeProp = pi.GetMethod.CreateDelegate<Func<Int32>>(obj);
 Console.WriteLine("SomeProp: " + getSomeProp());
 // Add and remove a delegate from the event
 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) {
 Console.WriteLine("UseDynamicToBindAndInvokeTheMember");
 // Construct an instance (You can't use dynamic to call a constructor)
 Object[] args = new Object[] { 12 }; // Constructor arguments
 Console.WriteLine("x before constructor called: " + args[0]);
 dynamic obj = Activator.CreateInstance(t, args);
 Console.WriteLine("Type: " + obj.GetType().ToString());
 Console.WriteLine("x after constructor returns: " + args[0]);
 // Read and write to a field 
 try {
 obj.m_someField = 5;
 Int32 v = (Int32)obj.m_someField;
 Console.WriteLine("someField: " + v);
 }
 catch (RuntimeBinderException e) {
 // We get here because the field is private
 Console.WriteLine("Failed to access field: " + e.Message);
 }
 // Call a method
 String s = (String)obj.ToString();
 Console.WriteLine("ToString: " + s);
 // Read and write a property
 try {
 obj.SomeProp = 0;
 }
 catch (ArgumentOutOfRangeException) {
 Console.WriteLine("Property set catch.");
 }
 obj.SomeProp = 2;
 Int32 val = (Int32)obj.SomeProp;
 Console.WriteLine("SomeProp: " + val);
 // Add and remove a delegate from the event
 obj.SomeEvent += new EventHandler(EventCallback);
 obj.SomeEvent -= new EventHandler(EventCallback);
 }
}
internal static class ReflectionExtensions {
 // Helper extension method to simplify syntax to create a delegate
 public static TDelegate CreateDelegate<TDelegate>(this MethodInfo mi, Object target = null) {
 return (TDelegate)(Object)mi.CreateDelegate(typeof(TDelegate), target);
 }
}

If you build and run this code, you’ll see the following output.

BindToMemberThenInvokeTheMember
x before constructor called: 12
Type: SomeType
x after constructor returns: 24
someField: 33
ToString: 33
Property set catch.
SomeProp: 2
BindToMemberCreateDelegateToMemberThenInvokeTheMember
x before constructor called: 12
Type: SomeType
x after constructor returns: 24
ToString: 0
Property set catch.
SomeProp: 2
UseDynamicToBindAndInvokeTheMember
x before constructor called: 12
Type: SomeType
x after constructor returns: 24
Failed to access field: 'SomeType.m_someField' is inaccessible due to its protection level
ToString: 0
Property set catch.
SomeProp: 2

Notice that SomeType’s constructor takes an Int32 by reference as its only parameter. The previous code shows how to call this constructor and how to examine the modified Int32 value after the constructor returns. Near the top of the BindToMemberThenInvokeTheMember method, I show how to accomplish this by calling Type’s GetType method passing in a string of "System.Int32&". The ampersand (&) in the string allows me to identify a parameter passed by reference. This ampersand is part of the Backus-Naur Form grammar for type names, which you can look up in the FCL documentation. The code also shows how to accomplish the same thing using Type’s MakeByRefType method.

# Using Binding Handles to Reduce Your Process’s Memory Consumption

Many applications bind to a bunch of types (Type objects) or type members (MemberInfo-derived objects) and save these objects in a collection of some sort. Then later, the application searches the collection for a particular object and then invokes this object. This is a fine way of doing things except for one small issue: Type and MemberInfo-derived objects require a lot of memory. So if an application holds on to too many of these objects and invokes them occasionally, the application’s memory consumption increases dramatically, having an adverse effect on the application’s performance.

Internally, the CLR has a more compact way of representing this information. The CLR creates these objects for our applications only to make things easier for developers. The CLR doesn’t need these big objects itself in order to run. Developers who are saving/caching a lot of Type and MemberInfoderived objects can reduce their working set by using run-time handles instead of objects. The FCL defines three runtime handle types (all defined in the System namespace): RuntimeTypeHandle, RuntimeFieldHandle, and RuntimeMethodHandle. All of these types are value types that contain just one field, an IntPtr; this makes instances of these types cheap (memory-wise). The IntPtr field is a handle that refers to a type, field, or method in an AppDomain’s loader heap. So what you need now is an easy and efficient way to convert a heavyweight Type/MemberInfo object to a lightweight run-time handle instance and vice versa. Fortunately, this is easy using the following conversion methods and properties:

  • To convert a Type object to a RuntimeTypeHandle, call Type’s static GetTypeHandle method passing in the reference to the Type object.

  • To convert a RuntimeTypeHandle to a Type object, call Type’s static GetTypeFromHandle method passing in the RuntimeTypeHandle.

  • To convert a FieldInfo object to a RuntimeFieldHandle, query FieldInfo’s instance read-only FieldHandle property.

  • To convert a RuntimeFieldHandle to a FieldInfo object, call FieldInfo’s static GetFieldFromHandle method.

  • To convert a MethodInfo object to a RuntimeMethodHandle, query MethodInfo’s instance read-only MethodHandle property.

  • To convert a RuntimeMethodHandle to a MethodInfo object, call MethodInfo’s static GetMethodFromHandle method.

The following program sample acquires a lot of MethodInfo objects, converts them to RuntimeMethodHandle instances, and shows the working set difference.

using System; 
using System.Reflection; 
using System.Collections.Generic; 
public sealed class Program { 
 private const BindingFlags c_bf = BindingFlags.FlattenHierarchy | BindingFlags.Instance | 
 BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic; 
 public static void Main() { 
 // Show size of heap before doing any reflection stuff
 Show("Before doing anything");
 // Build cache of MethodInfo objects for all methods in MSCorlib.dll
 List<MethodBase> methodInfos = new List<MethodBase>();
 foreach (Type t in typeof(Object).Assembly.GetExportedTypes()) {
 // Skip over any generic types
 if (t.IsGenericTypeDefinition) continue;
 MethodBase[] mb = t.GetMethods(c_bf);
 methodInfos.AddRange(mb);
 }
 // Show number of methods and size of heap after binding to all methods
 Console.WriteLine("# of methods={0:N0}", methodInfos.Count);
 Show("After building cache of MethodInfo objects");
 // Build cache of RuntimeMethodHandles for all MethodInfo objects
 List<RuntimeMethodHandle> methodHandles = 
 methodInfos.ConvertAll<RuntimeMethodHandle>(mb => mb.MethodHandle);
 Show("Holding MethodInfo and RuntimeMethodHandle cache");
 GC.KeepAlive(methodInfos); // Prevent cache from being GC'd early
 methodInfos = null; // Allow cache to be GC'd now
 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); // Prevent cache from being GC'd early
 GC.KeepAlive(methodInfos); // Prevent cache from being GC'd early
 methodHandles = null; // Allow cache to be GC'd now
 methodInfos = null; // Allow cache to be GC'd now
 Show("After freeing MethodInfos and RuntimeMethodHandles");
 }
}

When I compiled and executed this program, I got the following output.

Heap size= 85,000 - Before doing anything
# of methods=48,467
Heap size= 7,065,632 - After building cache of MethodInfo objects
Heap size= 7,453,496 - Holding MethodInfo and RuntimeMethodHandle cache
Heap size= 6,732,704 - After freeing MethodInfo objects
Heap size= 7,372,704 - Size of heap after re-creating MethodInfo objects
Heap size= 192,232 - After freeing MethodInfos and RuntimeMethodHandles

💡小结:字段、构造器、方法、属性、事件和嵌套类型都可以定义成类型的成员。FCL 包含抽象基类 System.Reflection.MemberInfo ,封装了所有类型成员都通用的一组属性。 MemberInfo 有许多派生类,每个都封装了与特定类型成员相关的更多属性。由于 MemberInfo 类是成员层次结构的根,所以有必要更深入地研究一下它,它提供的属性和方法是一个类型的所有成员都通用的。不要忘了 System.TypeInfoMemberInfo 派生。在查询 DeclaredMemebers 属性所返回的集合中,每个元素都会对层次结构中的一个具体类型的引用。虽然 TypeInfoDeclaredMembers 属性能返回类型的所有成员,但还可利用 TypeInfo 提供的一些方法返回具有指定字符串名称的成员类型。例如,利用 TypeInfoGetDeclaredNestedTypeGetDeclaredFieldGetDeclaredMethodGetDeclaredPropertyGetDeclaredEvent 方法,可分别返回一个 TypeInfoFieldInfoMethodInfoPropertyInfoEventInfo 对象引用。而利用 GetDeclaredMethods 方法能返回由 MethodInfo 对象构成的集合,这些对象描述了和指定字符串名称匹配的一个 (多个) 方法。基于 AppDomain,可发现其中加载的所有程序集。基于程序集,可发现构成它的所有模块。基于程序集或模块,可发现它定义的所有类型。基于类型,可发现它的嵌套类型、字段、构造器、方法、属性和事件。命名空间不是这个层次结构的一部分,因为它们只是从语法角度将相关类型聚集到一起。CLR 不知道什么是命名空间。要列出程序集中定义的所有命名空间,需枚举程序集中的所有类型,并查看其 Namespace 属性。基于一个类型,还可发现它实现的接口。基于构造器、方法、属性访问器方法或者事件的添加 / 删除方法,可调用 GetParameters 方法来获取由 ParameterInfo 对象构成的数组,从而了解成员的参数的类型。还可查询只读属性 ReturnParameter 获得一个 ParameterInfo 对象,它详细描述了成员的返回类型。对于泛型类型或方法,可调用 GetGenericArguments 方法来获得类型参数的集合。最后,针对上述任何一项,都可查询 CustomAttributes 属性来获得应用于它们的自定义定制特性的集合。发现类型定义的成员后可调用它们。“调用”(invoke) 的确切含义取决于要调用的成员的种类。 PropertyInfo 类型代表与属性有关的元数据信息;也就是说, PropertyInfo 提供了 CanReadCanWrite 、和 PropertyType 只读属性,它们指出属性是否可读和可写,以及属性的数据类型是什么。 PropertyInfo 还提供了只读 GetMethodSetMethod 属性,它们返回代表属性 getset 访问器方法的 MethodInfo 对象。 PropertyInfoGetValueSetValue 方法只是为了提供方法;在内部,它们会自己调用合适的 MethodInfo 对象。为了支持有参属性 (C# 的索引器), GetValueSetValue 方法提供了一个 Object[] 类型的 index 参数。 EventInfo 类型代表与事件有关的元数据信息。 EventInfo 类型提供了只读 EventHandlerType 属性,返回事件的基础委托的 TypeEventInfo 类型还提供了只读 AddMethodRemoveMethod 属性,返回为事件增删委托的方法的 MethodInfo 对象。增删委托可调用这些 MethodInfo 对象,也可调用 EventInfo 类型提供的更好用的 AddEventHandlerRemoveEventHandler 方法。许多应用程序都绑定了一组类型 ( Type 对象) 或类型成员 ( MemeberInfo 派生对象),并将这些对象保存在某种形式的集合中。以后,应用程序搜索这个集合,查找特定对象,然后调用 (invoke) 这个对象。这个机制很好,只是有个小问题: TypeMemberInfo 派生对象需要大量内存。所以,如果应用程序容纳了太多这样的对象,但只是偶尔调用,应用程序消耗的内存就会急剧增加,对应用程序的性能产生负面影响。CLR 内部用更精简的方式表示这种信息。CLR 之所以为应用程序创建这些对象,只是为了方便开发人员。CLR 不需要这些大对象就能运行。如果需要保存 / 缓存大量 TypeMemberInfo 派生对象,开发人员可以使用运行时句柄 (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