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

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

程序集加载

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

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

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

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

LoadFrom

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

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

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

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

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

  如果传递的是一个internet位置,clr会下载文件,把它安装到用户的下载缓存中,再从那儿加载文件。注意,当前必须联网,否则会抛出异常。但如果文件之前已下载过,而且ie被设置为脱机工作,就会使用以前下载的文件。

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

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

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

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

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

       使用ReflectionOnlyLoadFrom或ReflectionOnlyLoad方法加载的程序集表面上是可以卸载的。毕竟,这些程序集中的代码是不允许执行的。但CLR一样不允许卸载用这两个方法加载的程序集。因为用这两个方法加载了程序集之后,仍然可以利用反射来创建对象,以便引用这些程序集中定义的元数据。

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

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

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

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

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

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

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

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

       事实上,只有极少数应用程序才需要使用反射类型。如果类库需要理解类型的定义才能提供丰富的功能,就适合使用反射。例如,fcl的序列化机制就是利用反射来判断类型定义了哪些字段。然后,序列化格式器(serialiazation formatter)可获取这些字段的值,把它们写入字节流以便通过internet传送、保存到文件或复制到剪贴板。类似地,在设计期间,microsoft visual studio设计器在web窗体或windows窗体上放置控件时,也利用反射来决定要向开发人员显示的属性。

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

 

反射的性能

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

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

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

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

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

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

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

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

      

发现程序集中定义的类型

       反射经常用于判断程序集定义了哪些类型。Fcl提供了许多api来获取这方面的信息。目前常用的是assembl的exportedTypes属性

static void Main(string[] args)
{
    string dataAssembly = "System.Data,version=4.0.0.0," + "culture=neutral,PublicKeyToken=b77a5c561934e089";
    LoadAssemAndShowPublicTypes(dataAssembly);
}
private static void LoadAssemAndShowPublicTypes(string assemblyName)
{
    //显式地将程序集加载到这个appDomain中
    Assembly a = Assembly.Load(assemblyName);
    //在一个循环中显示已加载程序集中每个公开导出type全面
    foreach (Type t in a.ExportedTypes)
    {
        Console.WriteLine(t.FullName);
    }
}

 

类型对象的准确含义

  注意,上述代码遍历system.type对象构成的数组。system.type类型是执行类型和对象操作的起点。system.type对象代表一个类型引用(而不是类型定义)。

       总所周知,system.object定义了公共非虚实例方法getType。调用这个方法时,clr会判定指定对象的类型,并返回对该类型的type对象的引用。由于在一个appDomain中,每个类型只有一个type对象,所以可以使用相等和不相等操作符来判断两个对象是不是相同的类型。

       除了调用object的getType方法,fcl还提供了获得type对象的其他几种方式。

1 system.type类型提供了静态getType方法的几个重载版本。所有版本都接受一个string参数。字符串必须指定类型的全名。

2 system.typeinfo类型提供了实例成员DeclaredNestedTypes和GetDeclaredNestedType。

3 system.reflection.assembly类型提供了实例成员getType,definedtypes和exportedTypes。

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

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

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

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

Type typeReference=…;//例如o.gettype()或者typeof(Object)
TypeInfo typeDefinition=typeReference.getTypeInfo();=

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

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

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

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

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

private static void Go()
{
    //显示加载想要反射的程序集
    LoadAssemblies();
    //对所有类型进行筛选和排序
    var allTypes = (from a in AppDomain.CurrentDomain.GetAssemblies()
            from t in a.ExportedTypes
            where typeof(Exception).GetTypeInfo().IsAssignableFrom(t.GetTypeInfo())
            orderby t.Name
            select t).ToArray();
    //生成并显示继承层次结构
    Console.WriteLine(WalkInheritanceHierarchy(new StringBuilder(),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);
    }
}
构建exception 派生类型层次结构

相关文章: