程序集加载和反射

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

动态可扩展应用程序可利用第22章讲述的CLR技术和AppDomain. 宿主可以在一个AppDomain中运行加载项代码, 这个AppDomain有它自己的安全性和配置设置. 宿主还可通过卸载AppDomain来卸载加载项.

这里讨论的是.Net Framework 4.5引入的新的反射API.

程序集加载

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

在内部, CLR使用System.Reflection.Assembly类的静态Load方法尝试加载这个程序集. 这个方法是公开的, 可以显式地将程序集加载到AppDomain中. 该方法是CLR的与Win32LoadLibrary函数等价的方法.

public class Assmbly
{
   // 常用的重载的原型
   public static Assembly Load(AssemblyName assemblyRef);
   public static Assembly Load(String assemblyString);
   // 未列出不常用的函数
}

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

System.AppDomain提供了Load方法. 和Assembly静态Load方法不同, AppDomain的Load是实例方法,它允许将程序集加载到指定的AppDomain中. 该方法设计由非托管代码调用. 允许宿主将程序集注入特定AppDomain中.调用System.AppDomainLoad方法需要传递一个标识了程序集的字符串.

AppDomainLoad方法会返回对程序集的引用, 由于Assembly类不是从System.MarshallByRefObject派生的, 所以程序集对象必须按值封送回发出调用的AppDomain. 应该避免使用AppDomain的Load方法.

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

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

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

在内部,

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

LoadForm方法允许传递一个URL作为实参,如下:

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

如果传递的是一个internet位置,clr会下载文件,把它安装到用户的下载缓存中,再从那儿加载文件。注意,当前必须联网,否则会抛出异常。但如果文件之前已下载过,而且ie被设置为脱机工作,就会使用以前下载的文件, 不会抛出异常, 还可以调用UnsafeLoadFrom方法, 能够加载从网上下载的程序集,同时绕过一些安全检查.

VS的UI设计人员和其他工具一般用的是AssemblyLoadFile方法。这个方法可从任意路径加载程序集,而且可以将具有相同标识的程序集多次加载到一个AppDomain中。在设计器/工具中对应用程序的UI进行修改,而且用户重新生成了程序集时,便有可能发生这种情况。通过LoadFile加载程序集时,CLR不会自动解析任何依赖性问题;你的代码必须向AppDomain的AssemblyResolve事件登记,并让事件回调方法显式地加载任何依赖的程序集。

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

public class Assembly
{
    public static Assembly ReflectionOnlyLoadFrom(string assemblyFile);
    public static Assembly ReflectionOnlyLoad(string assemblyString);
}

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

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

CLR不提供卸载单独程序集的能力. 如果CLR允许这样做, 那么一旦线程从某个方法返回至已卸载的一个程序集中的代码, 应用程序就会崩溃, 健壮性和安全性是CLR最优先考虑的目标. 如果允许应用程序以这样的一种方式崩溃,就和它的设计初衷背道而驰了。卸载程序集必须卸载包含它的整个AppDomain。

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

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

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

    using (var stream=assem.GetManifestResourceStream(resourceName))
    {
        byte[] assemblyData=new byte[stream.Length];
        stream.Read(assemblyData, 0, assemblyData.Length);
        return Assembly.Load(assemblyData);
    }
}

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

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

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

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

有些反射类型是专门由CLR编辑器开发人员使用的, 应用程序的开发人员一般用不着.

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

反射的性能

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

  1. 反射造成编译时无法保证类型安全性。由于反射严重依赖字符串,所以会丧失编译时的类型安全性。例如,执行Type.GetType(“int”);要求通过反射在程序集中查找名为int的类型,代码会通过编译,但在运行时会返回null,因为CLR只知道System.Int32,不知道int

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

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

基于上述所有原因,最好避免利用反射来访问字段或调用方法/属性

应该利用以下两种技术之一开发应用程序来动态发现和构造类型实例。

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

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

发现程序集中定义的类型

反射经常用于判断程序集定义了哪些类型。FCL提供了许多api来获取这方面的信息。目前常用的是Assembly的ExportedTypes属性。 显示其中定义的所有公开导出的类型(也就是public类型).

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对象,所以可以使用相等和不相等操作符来判断两个对象是不是相同的类型。

o1.GetType() == o2.GetType();

除了调用Object的GetType方法,FCL还提供了获得Type对象的其他几种方式。

  1. System.Type类型提供了静态GetType方法的几个重载版本。所有版本都接受一个String参数。字符串必须指定类型的全名,而不是编辑器支持的基元类型(int,string,bool等), 这些名称对CLR没有任何意义。
  2. System.Type类型提供了静态的ReflectionOnlyGetType方法. 与上一条行为上相似, 只是类型会以”仅反射”的方式加载, 不能执行.
  3. System.TypeInfo类型提供了实例成员DeclaredNestedTypesGetDeclaredNestedType

  4. System.Reflection.Assembly类型提供了实例成员GetTypeDefinedTypesExportedTypes

参考文档: 指定完全限定的类型名称

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

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

上述代码中, 使用typeof是精确匹配, 而不是兼容匹配.

  • 精确匹配: 不检查是否从FileInfo类型派生的对象, 只检查是否引用了FileInfo类型对象.
  • 兼容匹配: 使用转型或者C#is/as操作符时, 测试的就是兼容匹配.

如前所述,Type对象是轻量级的对象引用。要更多地了解类型本身,必须获取一个TypeInfo对象,后者才代表类型定义。可调用System.Reflection.IntrospectionExtensionsGetTypeInfo扩展方法将Type对象转换成TypeInfo对象。

Type typeReference = o.GetType();//例如o.gettype()或者typeof(Object)
// 将Type对象转换成TypeInfo对象
TypeInfo typeDefinition = typeReference.GetTypeInfo();

// 将TypeInfo对象转换为Type对象
Type typeReference2 = typeDefinition.AsType();

另外,虽然作用不大,但还可调用TypeInfoAsType方法TypeInfo对象转换为Type对象

获取TypeInfo对象会强迫CLR确保已加载类型的定义程序集,从而对类型进行解析。这个操作可能代价高昂。如果只需要类型引用(Type对象),就应该避免这个操作。但一旦获得了TypeInfo对象,就可查询类型的许多属性进一步了解它。大多数属性,比如IsPublicisSealedisAbstractisClassisValueType等,都指明了与类型关联的标志。另一些属性,比如Assembly,AssemblyQualifiedName,FullName和Module等,则返回定义该类型程序集或模块的名称以及类型全名。还可查询BaseType属性来获取对类型的基类型的引用。除此之外,还有许多方法能提供关于类型的更多信息。

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

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

private static void Go()
{
    //显示加载想要反射的程序集
    LoadAssemblies();
    //对所有类型进行筛选和排序
    var allTypes = (from a in AppDomain.CurrentDomain.GetAssemblies()
            from t in a.ExportedTypes
            where typeof(Exception).GetTypeInfo().IsAssignableFrom(t.GetTypeInfo())
            orderby t.Name
            select t).ToArray();
    //生成并显示继承层次结构
    Console.WriteLine(WalkInheritanceHierarchy(new StringBuilder(),0,typeof(Exception),allTypes ));
}
private static StringBuilder WalkInheritanceHierarchy(StringBuilder sb ,int indent,Type baseType,IEnumerable<Type> allTypes)
{
    string spaces = new String(' ', indent * 3);
    sb.AppendLine(spaces + baseType.FullName);
    foreach (var t in allTypes)
    {
        if (t.GetTypeInfo().BaseType!=baseType)
        {
            continue;
        }
        WalkInheritanceHierarchy(sb, indent + 1, t, allTypes);
    }
    return sb;
}
private static void LoadAssemblies()
{
    string[] assemblies = {"System,PublicKeyToken={0}", "System.Core,PublicKeyToken={0}","System.Data,PublicKeyToken={0}","System.Design,PublicKeyToken={1}"};

    string ecmaPublicKeyToken = "b77a5c561934e089";
    string msPublicKeyToken = "b03f5f7f11d50a3a";
    //获取包含system.object的程序集的版本,假定其他所有程序集都是相同的版本
    Version version = typeof(System.Object).Assembly.GetName().Version;
    //显示加载想要反射的程序集
    foreach (var a in assemblies)
    {
        string assemblyIdentity = string.Format(a, ecmaPublicKeyToken, msPublicKeyToken) +
                                  ",Culture=neutral,Version=" + version;
        Assembly.Load(assemblyIdentity);
    }
}

构造类型的实例

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

  1. System.ActivatorCreateInstance方法

Activator类提供了静态CreateInstance方法的几个重载版本. 可以传递Type对象引用, 也可以传递标识了类型的String, 直接获取类型对象的几个版本较为简单。你要为类型的构造器传递一组实参,方法返回新对象的引用。

用字符串来制定类型的几个版本稍微复杂一些。首先必须指定另一个字符串来表示定义了类型的程序集。其次,如果正确配置了远程访问(remoting)选项,这些方法还允许构造远程对象, 这些版本返回的不是新对象的引用, 而是一个System.Runtime.Remoting.ObjectHandle对象(从MarshalByRefObject派生). ObjectHandle允许将一个AppDomain中创建的对象传至其他AppDomain. 期间不强迫对象具体化(materialize) , 准备好具体化这个对象时, 它将定义了要具体化的类型的程序集加载到这个AppDomain中. 如果对象按引用封送, 会创建代理类型和对象. 如果按值封送, 对象的副本会被序列化.

  1. System.ActivatorCreateInstanceForm方法

Activator类还提供了一组静态CreateInstanceForm方法,他们与CreateInstance的行为相似,只是必须通过字符串参数来指定类型及其程序集。程序集用Assembly的LoadForm(而非load)方法加载到调用AppDomain中。由于都不接受Type参数,所以返回的都是一个ObjectHandle对象引用,必须调用ObjectHandleUnwrap方法进行具体化.

  1. System.Appdomain的方法

Appdomain类型提供了4个用于构造类型实例的实例方法,包括CreateInstanceCreateInstanceFromCreateInstanceFromAndUnwrap。这些方法和行为和Activator类的方法相似。区别在于他们都是实例方法,允许指定在哪个Appdomain中构造对象。另外,带Unwrap后缀的方法还能简化操作,不必执行额外的方法调用。

  1. System.Reflection.ConstructorInfoInvoke实例方法

使用一个Type对象引用,可以绑定到一个特定的构造器,并获取对构造器的ConstructorInfo对象的引用。然后,可利用ConstructorInfo对象引用来调用它的Invoke方法。类型总是在调用Appdomain中创建,返回的是对新对象的引用。

注意: CLR不要求值类型定义任何构造器。ActivatorCreateInstance方法允许在不调用构造器的情况下创建值类型的实例。必须调用CreateInstance方法获取单个Type参数的重载版本或者获取Type和Boolean参数的重载版本。

利用前面列出的机制,可为除数组(System.Array派生类型)委托(System.MulticastDelegate派生类型) 之外的所有类型创建对象

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

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

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

internal sealed class Dictionary<TKey,TValue>{}

public static 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]
    }
}

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

构建可扩展应用程序时, 接口是中心. 可用基类代替接口, 但接口通常是首选的. 因为它允许加载项开发人员选择他们自己的基类. 例如要写一个应用程序来无缝加载和使用别人的类型. 下面描述了如何设计:

  • 创建宿主SDK程序集, 它定义一个接口. 接口的方法作为宿主应用程序加载项之间的通信机制使用. 接口方法定义参数和返回类型时, 请尝试使用MSCorLib.dll中定义的其他接口或类型. 要传递并返回自己的数据类型, 也在宿主SDK程序集中定义. 一定搞定接口定义, 就可为这个程序集赋予强名称. 然后打包并部署到用户那里. 发布以后要避免对该程序集中的类型做出任何重大的改变. 例如: 不要以任何方式更改接口. 如果定义了任何数据类型, 在类型中添加新成员时完全允许的.

之所以能用MSCorLib.dll中定义的类型, 是因为CLR总是加载与CLR本身的版本匹配的MSCorLib.dll. 此外, 一个CLR实例只会加载一个版本的MSCorLib.dll.

  • 加载项开发人员会在加载项程序集中定义自己的类型. 这些程序集引用你的宿主程序集中的类型. 加载项开发人员可按自己的步调推出程序集的新版本, 而宿主应用程序能正常使用加载项中的类型.

  • 创建单独的”宿主应用程序“ 程序集, 在其中包含你的应用程序的类型, 这个程序集显然要引用宿主SDK, 并使用其中定义的类型. 可自由修改宿主应用程序程序集的代码, 由于加载项开发人员不会引用这个宿主应用程序程序集, 所以随时都能退出宿主应用程序程序集的新版本. 不会对加载项开发人员产生任何影响.

跨程序集使用类型时, 需要关注程序集的版本控制问题. 如果要修改,一定要修改程序集的版本号.

下面演示一个例子:

using System;
// HostSDK.dll程序集的代码
namespace Wintellect.HostSDK
{
    public interface IAddIn
    {
        String DoSomething(Int32 x);
    }
}
using System;
using Wintellect.HostSDK;

// AddInTypes.dll程序集的代码 其中定义了两个公共类型
// 实现了HostSDK.dll的接口
public class AddIn_A : IAddIn
{
    public AddIn_A()
    {
    }

    public String DoSomething(Int32 x)
    {
        return "AddIn_A: " + x.ToString();
    }
}

public class AddIn_B : IAddIn
{
    public AddIn_B()
    {
    }

    public String DoSomething(Int32 x)
    {
        return "AddIn_B: " + (x * 2).ToString();
    }
}

然后试一个简单的Host.exe程序集(控制台应用程序的代码). 生成该程序集必须引用HostSDK.dll.

using System;
using System.Linq;
using System.IO;
using System.Reflection;
using System.Collections.Generic;
using Wintellect.HostSDK;

public sealed class Program
{
    public static void Main()
    {
        // 查找Host.exe所在的目录
        // F:\迅雷下载\新建文件夹\bin\Debug
        String AddInDir = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);

        // 假定加载项程序集和Host.exe文件在同一目录
        // 返回满足指定条件的文件名的可枚举集合。
        // F:\迅雷下载\新建文件夹\bin\Debug\Ch23-2-AddIn_SDKAssembly.dll
        var AddInAssemblies = Directory.EnumerateFiles(AddInDir, "*.dll");

        // Ch23-2-AddIn_SDKAssembly, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
        var test = Assembly.LoadFrom(@"F:\迅雷下载\新建文件夹\bin\Debug\Ch23-2-AddIn_SDKAssembly.dll");
        // Console.WriteLine(test.GetName());

        var test1 = Assembly.Load("Ch23-2-AddIn_SDKAssembly, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null");
        // Console.WriteLine(test1.GetName());


        // 创建可由宿主使用的所有加载Type的一个集合
        var AddInTypes =
            from file in AddInAssemblies
            let assembly = Assembly.LoadFrom(file) // 这里源代码错了, 书中代码写的是Assembly.Load, 通过给定程序集的长格式名称加载程序集。
            from t in assembly.ExportedTypes // 公开导出的类型
            // 如果类型实现了IAddIn接口, 该类型就可由宿主使用
            where t.IsClass && typeof(IAddIn).GetTypeInfo().IsAssignableFrom(t.GetTypeInfo())
            select t;
        // 初始化完成: 宿主已发现所有可用的加载项

        // 下面示范宿主如何构造加载项对象并使用
        foreach (Type t in AddInTypes)
        {
            IAddIn ai = (IAddIn) Activator.CreateInstance(t);
            Console.WriteLine(ai.DoSomething(5));
        }
    }
}
// AddIn_A: 5
// AddIn_B: 10

使用反射发现类型的成员

本章剩余部分将从其他角度探讨反射,目的是发现并调用类型的成员。

发现类型的成员

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

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

using System;
using System.Reflection;

class Program
{
    static void Main(string[] args)
    {
        //遍历这个appDomain中加载的所有程序集
        Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();
        foreach (var a in assemblies)
        {
            // 缩进0*3个空格, a代表单个程序集
            // Assembly:mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
            Show(0, "Assembly:{0}", a);
            //查找程序集中的类型
            // a.ExportedTypes 是所有类型的集合
            foreach (var t in a.ExportedTypes)
            {
                //    Assembly:Microsoft.Win32.Registry
                // 缩进1*3个空格 t代表单个导出类型
                Show(1, "Assembly:{0}", t);
                //发现类型的成员
                // t.GetTypeInfo().DeclaredMembers 类型的所有成员
                foreach (var mi in t.GetTypeInfo().DeclaredMembers)
                {
                    string typeName                     = string.Empty;
                    if (mi is Type) typeName            = "(Nested) Type";
                    if (mi is FieldInfo) typeName       = "FieldInfo";
                    if (mi is MethodInfo) typeName      = "MethodInfo";
                    if (mi is ConstructorInfo) typeName = "ConstructorInfo";
                    if (mi is PropertyInfo) typeName    = "PropertyInfo";
                    if (mi is EventInfo) typeName       = "EventInfo";

                    Show(2, "{0}:{1}", typeName, mi);
                }
            }
        }
    }

    private static void Show(int indent, string format, params object[] args)
    {
        Console.WriteLine(new string(' ', 3 * indent) + format, args);
    }
}

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

总结了用于遍历反射对象模型的各种类型:

  • 基于AppDoamin,可发现其中加载的所有程序集, 可发现它的所有模块.
  • 基于程序集或模块, 可发现它定义的所有类型.
  • 基于类型,可以发现它的嵌套类型,字段,构造器,方法,属性和事件.

命名空间不是这个层次结构的一部分.因为它们只是从语法角度将相关类型聚集到一起。CLR不知道什么是命名空间。要列出程序集中定义的所有命名空间,需枚举程序集中的所有类型,并查看其Namespace属性

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

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

调用类型的成员

发现类型定义的成员后可调用它们. “调用”(invoke)的确切含义取决于要调用的成员的种类.

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

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

以下实例应用程序演示了用反射来访问类型成员的各种方式。

SomeType类包含多种成员:一个私有字段(m_someField);一个公共构造器(SomeType),它获取一个传引用的Int32实参;一个公共方法(ToString);一个公共属性(SomeProp);以及一个公共事件(SomeEvent)。定义好SomeType类型后,我提供了三个不同的方法,他们利用反射来访问SomeType的成员。三个方法用不同的方式做相同的事情。

  1. BindToMemberThenInvokeTheMember方法,演示了如何绑定到成员并调用它。
  2. BindToMemberCreateDelegateToMemberThenInvokeTheMember方法演示了如何绑定到一个对象或成员,然后创建一个委托来引用该对象或成员。通过委托来调用的速度很快。如果需要在相同的对象上多次调用相同的成员,这个技术的性能比上一个好
  3. UseDynamicToBindAndInvokeTheMember方法演示了如何利用C#的dynamic基元类型简化成员访问语法。此外,在相同类型的不同对象上调用相同成员时,这个技术还能提供不错的性能,因为针对每个类型,绑定都只会发生一次。而且可以缓存起来,以后多次调用的速度会非常快。用这个计数也可以调用不同类型的对象的成员。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Microsoft.CSharp.RuntimeBinder;

internal sealed class SomeType
{
    private int m_someField;

    public SomeType(int x)
    {
        x *= 2;
    }

    public override string ToString()
    {
        return m_someField.ToString();
    }

    public int SomeProp
    {
        get { return m_someField; }
        set
        {
            if (value < 1)
            {
                throw new ArgumentOutOfRangeException("value");
            }

            m_someField = value;
        }
    }

    public event EventHandler SomeEvent;

    private void NoCompilerWarnings()
    {
        SomeEvent.ToString();
    }
}

class Program
{
    static void Main(string[] args)
    {
        Type t = typeof(SomeType);
        BindToMemberThenInvokeTheMember(t);
        Console.WriteLine();

        BindToMemberCreateDelegateToMemberThenInvokeTheMember(t);
        Console.WriteLine();

        UseDynamicToBindAndInvokeTheMember(t);
        Console.WriteLine();
    }

    private static void BindToMemberThenInvokeTheMember(Type t)
    {
        Console.WriteLine("BindToMemberThenInvokeTheMember");
        //构造实例
        Type ctorArgument = Type.GetType("System.Int32");
        //或者typeof(Int32).MakeByRefType();

        // 获取当前类型t定义的构造函数集合
        IEnumerable<ConstructorInfo> ctors = t.GetTypeInfo().DeclaredConstructors;

        // 找出参数是Int32类型的构造函数
        ConstructorInfo ctor  = ctors.First(c => c.GetParameters()[0].ParameterType == ctorArgument);
        //ConstructorInfo ctor = t.GetTypeInfo().DeclaredConstructors
        //    .First(c => c.GetParameters()[0].ParameterType == ctorArgument);

        //构造器的实参
        object[] args = new object[] {12};

        Console.WriteLine("x before constructor called:" + args[0]);
        // 用绑定好的ConstructorInfo去调用Invoke并传入参数args
        object obj = ctor.Invoke(args);
        Console.WriteLine("Type" + obj.GetType());
        Console.WriteLine("x after constructor returns" + args[0]);

        //读写字段
        FieldInfo fi = obj.GetType().GetTypeInfo().GetDeclaredField("m_someField");
        fi.SetValue(obj, 33);
        Console.WriteLine("someField:" + fi.GetValue(obj));

        //调用方法
        MethodInfo mi = obj.GetType().GetTypeInfo().GetDeclaredMethod("ToString");
        string     s  = (string) mi.Invoke(obj, null);
        Console.WriteLine("ToString:" + s);

        //读写属性
        PropertyInfo pi = obj.GetType().GetTypeInfo().GetDeclaredProperty("SomeProp");
        try
        {
            pi.SetValue(obj, 0, null);
        }
        catch (TargetInvocationException e)
        {
            if (e.InnerException.GetType() != typeof(ArgumentOutOfRangeException))
            {
                throw;
            }

            Console.WriteLine("Property set catch ");
        }

        pi.SetValue(obj, 2, null);
        Console.WriteLine("SomeProp:" + pi.GetValue(obj, null));

        //为事件添加和删除委托
        EventInfo    ei = obj.GetType().GetTypeInfo().GetDeclaredEvent("SomeEvent");
        EventHandler eh = new EventHandler(EventCallback);
        ei.AddEventHandler(obj, eh);
        ei.RemoveEventHandler(obj, eh);
    }

    //添加到事件的回调方法
    private static void EventCallback(object sender, EventArgs e)
    {
    }

    private static void BindToMemberCreateDelegateToMemberThenInvokeTheMember(Type t)
    {
        Console.WriteLine("BindToMemberCreateDelegateToMemberThenInvokeTheMember");

        //构造实例()不能创建对构造器的委托
        Object[] args = new object[] {12};

        Console.WriteLine("x before constructor called:" + args[0]);
        object obj = Activator.CreateInstance(t, args);
        Console.WriteLine("Type" + obj.GetType());
        Console.WriteLine("x after constructor returns" + args[0]);

        //注意:不能创建对字段的委托

        //调用方法
        MethodInfo mi = obj.GetType().GetTypeInfo().GetDeclaredMethod("ToString");
        var toString = mi.CreateDelegate<Func<string>>(obj);
        string s  = toString();
        Console.WriteLine("ToString:" + s);

        //读写属性
        PropertyInfo pi          = obj.GetType().GetTypeInfo().GetDeclaredProperty("SomeProp");
        var          setSomeProp = pi.SetMethod.CreateDelegate<Action<int>>(obj);
        try
        {
            setSomeProp(0);
        }
        catch (ArgumentOutOfRangeException)
        {
            Console.WriteLine("Property set catch ");
        }

        setSomeProp(2);
        var getSomeProp = pi.GetMethod.CreateDelegate<Func<int>>(obj);
        Console.WriteLine("SomeProp:" + getSomeProp());

        //为事件添加和删除委托
        EventInfo ei           = obj.GetType().GetTypeInfo().GetDeclaredEvent("SomeEvent");
        var       addSomeEvent = ei.AddMethod.CreateDelegate<Action<EventHandler>>(obj);
        addSomeEvent(EventCallback);
        var removeSomeEvent = ei.RemoveMethod.CreateDelegate<Action<EventHandler>>(obj);
        removeSomeEvent(EventCallback);
    }

    private static void UseDynamicToBindAndInvokeTheMember(Type t)
    {
        //构造实例()不能创建对构造器的委托
        Object[] args = new object[] {12};

        Console.WriteLine("x before constructor called:" + args[0]);
        dynamic obj = Activator.CreateInstance(t, args);
        Console.WriteLine("Type" + obj.GetType());
        Console.WriteLine("x after constructor returns" + args[0]);

        //读写字段

        try
        {
            obj.m_someField = 5;
            int v = (int) obj.m_someField;
            Console.WriteLine("someField:" + v);
        }
        catch (RuntimeBinderException e)
        {
            Console.WriteLine("failed to access field: " + e.Message);
        }

        //调用方法

        string s = (string) obj.ToString();
        Console.WriteLine("ToString:" + s);

        //读写属性
        try
        {
            obj.SomeProp = 0;
        }
        catch (ArgumentOutOfRangeException e)
        {
            Console.WriteLine("Property set catch ");
        }

        obj.SomeProp = 2;
        int val = (int) obj.SomeProp;
        Console.WriteLine("SomeProp:" + val);

        //为事件添加和删除委托
        obj.SomeEvent += new EventHandler(EventCallback);
        obj.SomeEvent -= new EventHandler(EventCallback);
    }
}

internal static class ReflectionExtensions
{
    public static TDelegate CreateDelegate<TDelegate>(this MethodInfo mi, object target = null)
    {
        return (TDelegate) (Object) mi.CreateDelegate(typeof(TDelegate), target);
    }
}

// BindToMemberThenInvokeTheMember
// x before constructor called:12
// TypeConsoleApp2.SomeType
// x after constructor returns12
// someField:33
// ToString:33
// Property set catch
// SomeProp:2
//
// BindToMemberCreateDelegateToMemberThenInvokeTheMember
// x before constructor called:12
// TypeConsoleApp2.SomeType
// x after constructor returns12
// ToString:0
// Property set catch
// SomeProp:2
//
// x before constructor called:12
// TypeConsoleApp2.SomeType
// x after constructor returns12
// failed to access field: “ConsoleApp2.SomeType.m_someField”不可访问,因为它具有一定的保护级别
// ToString:0
// Property set catch
// SomeProp:2

第三种方式简化了成员访问语法. 需要熟悉.

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

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

CLR内部用更精简的方式表示这种信息。CLR之所以为应用程序创建这些对象,只是为了方便开发人员。CLR不需要这些大对象就能运行。如果需要保存/缓存大量Type和MemberInfo派生对象,开发人员可以使用句柄(Runtime Handle)代替对象以减小工作集内存。FCL定义了三个运行时句柄类型(全部都在System命名空间),包括RuntimeTypeHandleRuntimeFieldHandleRuntimeMethodHandle三个类型都是值类型,都只包含一个字段,也就是一个IntPtr;这使类型的实例显得相当精简IntPtr字段是一个句柄引用AppDomain的Loader堆中的一个类型、字段或方法。因此,现在需要以一种简单、高效的方式将重量级的Type或MemberInfo对象转换为轻量级的运行时句柄实例,反之亦然。幸好,使用以下转换方法和属性可轻松达到目的。

  • Type对象转为RuntimeTypeHandle, 调用Type的静态GetTypeHandle方法并传递那个Type对象引用.
    • 反向转换, 调用Type的静态方法GetTypeFromHandle并传递那个RuntimeTypeHandle对象引用.
  • 要将FieldInfo对象转为RuntimeTypeHandle, 查询FieldInfo的实例只读属性FieldHandle.
    • 反向转换, 调用FieldInfo静态方法GetFieldFromHandle.
  • 要将MethodInfo对象转换为一个RuntimeMethodHandle, 查询MethodInfo的实例只读属性MethodHandle.
    • 反向转换, 调用MethodInfo静态方法GetMethodFromHandle.

以下实例程序获取许多MethodInfo对象,把它们转换为RuntimeMethodHandle实例,并演示了转换前后的工作集的差异。

using System;
using System.Collections.Generic;
using System.Reflection;

public class Program
{
    private const BindingFlags c_bf = BindingFlags.FlattenHierarchy | BindingFlags.Instance | BindingFlags.Static |
                                      BindingFlags.Public | BindingFlags.NonPublic;

    static void Main(string[] args)
    {
        //显示在任何反射操作之前堆的大小
        Show("做任何事之前"); // 刚开始占用的22384字节数

        //为MSCorlib.dll中所有方法构建MethodInfo对象缓存
        List<MethodBase> methodInfos = new List<MethodBase>();
        foreach (Type t in typeof(object).Assembly.GetExportedTypes())
        {
            //跳过任何泛型类型
            if (t.IsGenericTypeDefinition)
            {
                continue;
            }

            // 获取位标志BindingFlags 的方法
            MethodBase[] mb = t.GetMethods(c_bf);
            // Add:添加单个元素
            // AddRange:添加实现了接口IEnumerable<T>的一个泛型集合的所有元素到指定泛型集合末尾
            // 可以直接添加somelist.ToArray()
            methodInfos.AddRange(mb);
        }

        //显示当绑定所有方法之后,方法的个数和堆的大小
        Console.WriteLine("# 方法个数:={0:N0}", methodInfos.Count); // 有54333个方法
        Show("在创建了MethodInfo对象的缓存之后");// 完成缓存后占用了3956516字节数

        //为所有methodINFO对象构建RuntimeMethodHandle缓存
        List<RuntimeMethodHandle> methodHandles = methodInfos.ConvertAll<RuntimeMethodHandle>(c => c.MethodHandle);

        Show("保存MethodInfo和RuntimeMethodHandle缓存");
        GC.KeepAlive(methodInfos);//阻止缓存被过早垃圾回收

        methodInfos = null;//现在允许缓存垃圾回收
        Show("在释放methodinfo对象之后");


        methodInfos = methodHandles.ConvertAll<MethodBase>(c => MethodBase.GetMethodFromHandle(c));
        Show("重新创建methodInfo对象后堆的大小");


        GC.KeepAlive(methodHandles); //组织缓存被过早垃圾回收
        GC.KeepAlive(methodInfos);//组织缓存被过早垃圾回收

        methodHandles = null; //现在允许缓存垃圾回收
        methodInfos = null;//现在允许缓存垃圾回收

        Show("释放methodInfos和runtimemethodhandle之后");

    }

    private static void Show(string s)
    {
        Console.WriteLine("Heap size={0,2:N0}-{1}", GC.GetTotalMemory(true), s);
    }
}

// Heap size=22,092-做任何事之前
// # 方法个数:=54,333
// Heap size=4,095,280-在创建了MethodInfo对象的缓存之后
// Heap size=4,312,680-保存MethodInfo和RuntimeMethodHandle缓存
// Heap size=3,956,288-在释放methodinfo对象之后
// Heap size=4,173,700-重新创建methodInfo对象后堆的大小
// Heap size=82,596-释放methodInfos和runtimemethodhandle之后

RuntimeTypeHandleRuntimeFieldHandleRuntimeMethodHandle三个类型都是值类型,都只包含一个字段,也就是一个IntPtr;这使类型的实例显得相当精简