《CLR via C#》第22章 程序集加载和反射

这一章所有的东东都是关于发现Type的信息的,在编译时完全不知道该Type任何信息的情况下,创建一个Type的实例,访问Type的成员等。本章的这些信息典型的应用就是创建一个可以动态扩展的应用程序。可扩展的应用程序就是一个公司写一个主应用,由其他公司来写“插件”从而来扩展这个主应用。主应用不能针对这个“插件”(add-in)来创建或测试,因为这些“插件”是不同的公司写的或“插件”是在主应用卖出去以后才写的。这就是为什么主应用需要在运行时发现“插件”中的信息。

    一个动态扩展的应用程序会利用21章所介绍的“公共语言运行时(CLR)宿主和应用程序域(AppDomain)”。宿主会在一个有自己的安全和配置的 AppDomain中运行“插件”。宿主也可以通过卸载这个AppDomain来卸载“插件”(add-in)。本章结束时,我会讲一些关于如何把所有刚才说的统统放到一起:CLR宿主、AppDomain、程序集加载、发现Type、构建Type实例和反射,目的是为了创建一个健壮、安全、可动态扩展的应用程序。

程序集加载

    如你所知,当JIT编译器将一个方法编译成中间语言(IL)时,它会看看IL代码中引用了哪些Type。然后在运行时,JIT编译器利用程序集的 “TypeRef”和“AssemblyRef”元数据表来决定程序集引用了哪些Type。“AssemblyRef”元数据表项包含了程序集的强名称(strong name)的所有组成部分。JIT编译器会将所有的这些部分链接起来--名称(没有扩展名和路径)、版本、文化和公匙--形成一个串,然后试图加载与标识信息相匹配的程序集到AppDomain中(如果该程序集还没有被加载的话)。如果被加载的程序集是“弱名称”(weakly named),那么这个标识信息中仅仅有程序集的名字(没有版本、文化或公匙信息)。

    在内部,CLR试图用System.Reflection.Assembly类的静态方法Load来加载程序集。这个方法是公开地文档化了,所以你可以调用它,来显式地加载一个程序集到你的AppDomain中。这个CLR中的静态方法相当于Win32中的LoadLibrary函数。实际上 Assembly的Load方法有几个版本的重载。这有几个很常用的重载函数的原型:

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

    在内部,Load会促使CLR对程序集应用一个“版本-绑定重定向”策略,并且会在下面几个地方查找程序集:GAC中,应用程序的基础目录,私目录的子目录和基本代码处。如果你以弱名称程序集调用Load方法,Load方法不会对程序集应用一个“版本-绑定重定向”策略,并且CLR不会在GCA中查找该程序集。如果Load方法找到指定的程序集,它会返回一个Assembley对象的引用,该对象代表已加载的程序集。如果Load方法没有找到指定的程序集,它会抛出一个System.IO.FileNotFoundException。
   
    注意:在一些极少的情况下,你可能会想加载一个为一个特定版本的Windows创建的程序集。在这种情况下,当指定一个程序集身份信息时,你可以包括一个 “处理器架构”的部分。例如:如果我的全局程序集缓存(GAC)中有一个程序集的一个IL中立版本和一个特定的X86版本,CLR会偏爱与特定CPU有关的那个版本的程序集。然而,我可以通过给Assembly的Load方法传递下面的字符串来强制CLR加载IL中立版本的程序集:

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

今天,CLR为处理器架构这个问题,支持四个可能的值:MSIL,X86,IA64,AMD64。

  • Assembly类的静态方法Load是加载一个程序集到一个AppDomain中的首先方法。
  • AppDomain 的Load方法是一个非静态方法,它允许加载一个程序集到一个指定的AppDomain中。但这个方法设计上是被非托管代码调用的。这个方法允许宿主程序把一个程序集注入到一个指定的AppDomain中。托管代码的开发人员一般情况下不应该调用这个方法。因为会有一些与加载程序集的路径问题。
  • 如果想调用一个指定路径下的程序集,可以调用Assembly的LoadFrom方法。Assembly.LoadFrom内部会调用 Assembly.Load方法。如果Assembly.Load通过“版本-绑定重定向”策略和在其他有关的目录下没有找到指定的程序集文件,它才会用 LoadFrom参数中提供的路径信息。
  • Assembly.LoadFrom方法可以这样调用:Assembly a = Assembly.LoadFrom(@"http://Wintellect.com/SomeAssembly.dll");当你提供了一个 Internet地址,CLR会下载这个文件到“用户下载缓存”中,然后再从那里加载该程序集。这时如果你没有联到互联网的话,那么会抛出一个异常。然而,如果这个文件以前已经下载过,并且IE已经设置成“离线”工作,那么CLR就加载以前下载的那个文件,而不抛出异常。
  • 如果你想加载指定位置上的程序集,而不要CLR应用任何的查找策略,可以调用Assembly类的LoadFile方法。
  • 如果你只是想通过反射来分析一个程序集中元数据,而不会执行程序集中的代码,那么Assembly的静态方法ReflectionOnlyLoadFrom和ReflectionOnlyLoad比较适合。
  • CLR不支持卸载单独的程序集的功能。
  • 如果你想卸载一个程序集,你必须卸载包括这个程序集的整个AppDomain。


运用反射来建立可动态扩展的应用程序


    应该知道一些反射类型或一些类型的某些方法是为那些写CLR编译器的开发人员而专门设计的。应用程序开发人员典型情况下用不到这些类型和方法。

反射的性能

    反射也被用在一些情况中,例如当一个应用程序需要在运行时加载特别的程序集中的特别的类型来完成一些工作。一个应用程序可能要求用户提供程序集和类型的名字,然后这个应用程序可能显式地加载这个程序集,构造指定的类型的实例,并调用此类型定义的方法。

    反射是一个极其强大的机制,因为它允许你在运行时发现并使用在编译时不知道的类型和成员。这种强大也带来了两方面主要的缺点:

1、反射阻止编译时的类型安全机制。由于反射严重依赖于字符串,于是在编译时,你就失去了类型安全。例如Type.GetType("Jef");如果这个"Jef"拼写错误,你只能得到一个运行时错误。
2、反射是慢的。当使用反射机制时,类型的名字和成员在编译时都是未知的。你只能在运行时通过字符串形式的名字去唯一地识别类型和类型定义的方法。这就意味着反射要经常地执行字符串搜索,在System.Reflection名字空间扫描整个的程序集的元数据以获得类型。一般情况下,这种字符串搜索是大小写敏感的比较,这进一步减慢了速度。

    使用反射调用一个成员,也会伤害到性能。当使用反射来调用一个方法时,你必须先将参数打包到一个数组中。在内部,反射必须在线程栈上解包这个数组。因此,CLR必须在执行一个方法之前检查这些参数有正确的数据类型。最后,CLR要确定调用者有适当的安全许可来访问这个被调用的成员。

    因为所有的这些原因,最好避免使用反射来访问一个成员。如果你正在写一个动态发现并动态创建类型实例的应用程序,你应该采用以下的方法:

  • 以一个编译时已知的类型作为基类来派生需要动态创建的类型。在运行时,创建一个子类型的实例,然后以基类的变量来引用此实例(通过强制类型转换),最后调用基类类型定义的虚方法。
  • 让需要动态创建的类型去实现一个编译时类型可知的接口(interface)。在运行时,创建一个子类型的实例,然后以接口类型的变量来引用到这个实例(通过强制类型转换),调用接口中的定义的方法。(这种方法能好一些,因为第一种通过基类的方法,在一些特殊的情况下,不允许开发人员选择最适合的基类。)

    当你采用以上的两种方法时,强列建议那个接口和基类要定义在在自己的程序集中。这会减少版本问题。

发现一个程序集中定义的Type

Assembly的GetExportedTypes方法可以得到一个程序集的所有公共类型,这些公共类型在程序集外可见。

代码:

String dataAssembly = "System.Data, version=2.0.0.0, " +
"culture=neutral, PublicKeyToken=b77a5c561934e089";
Assembly a = Assembly.Load(dataAssembly );
foreach (Type t in a.GetExportedTypes()) {
Console.WriteLine(t.FullName);
}


一个Type对象倒底是什么东东?

System.Type 是从System.Reflection.MemberInfo继承的抽象类型。FCL中的 System.RuntimeType,System.ReflectionOnlyType,System.Reflection.TypeDelegator 类都是从System.Type中继承而来。System.Reflection.Emit名字空间中的EnumBuilder, GenericTypeParameterBuilder 和TypeBuilder也是从System.Type中继承而来。

  • TypeDelegator类通过封装Type可以代码动态地子类化一个Type,允许你覆盖一些方法,但用原来的Type处理大多数的工作。这一强大的机制允许你覆盖反射工作的方式。
  • System.RuntimeType 是Framework Class Library(FCL)内部的类型,在外部是看不到的,即此类型没有文档描述。当调用System.Object的GetType方法时,此方法就返回 RuntimeType的引用。在一个AppDomain中一个Type只有一个RuntimeType对象。可以如下验证:

    object o1 = new ArrayList();
    object o2 = new ArrayList();
    if(o1.GetType() == o2.GetType())
    {
    .......
    }
  • 运算符typeof在编译时就可以确定对象的类型(早绑定),而GetType方法在运行时得到对象的类型(晚绑定):
    private static void SomeMethod(Object o) {
    // GetType returns the type of the object at run time (late-bound)
    // typeof returns the type of the specified class (early-bound)
    if (o.GetType() == typeof(FileInfo)) { ... }  //----(1)
    if (o.GetType() == typeof(DirectoryInfo)) { ... }
    }
    注意:行(1)的代码检查变量o是不是引用到一个FileInfo类型上,它不检查o所引用的对象是不是引用到FileInfo的子类型上。即它检查的是精确匹配的类型,而不是兼容的类型。兼容类型的检查是is和as运算符所做的。


建立类的异常继承类型层次

    public static class ProgramTest
    {
        public static void Main()
        {
            // Explicitly load the assemblies that we want to reflect over
            LoadAssemblies();
            // Initialize our counters and our exception type list
            Int32 totalPublicTypes = 0, totalExceptionTypes = 0;
            List<String> exceptionTree = new List<String>();
            // Iterate through all assemblies loaded in this AppDomain
            foreach (Assembly a in AppDomain.CurrentDomain.GetAssemblies())
            {
                // Iterate through all types defined in this assembly
                foreach (Type t in a.GetExportedTypes())
                {
                    totalPublicTypes++;
                    // Ignore type if not a public class
                    if (!t.IsClass || !t.IsPublic) continue;
                    // Build a string of the type's derivation hierarchy
                    StringBuilder typeHierarchy = new StringBuilder(t.FullName, 5000);
                    // Assume that the type is not an Exception-derived type
                    Boolean derivedFromException = false;
                    // See if System.Exception is a base type of this type
                    Type baseType = t.BaseType;
                    while ((baseType != null) && !derivedFromException)
                    {
                        // Append the base type to the end of the string
                        typeHierarchy.Append("-" + baseType);
                        derivedFromException = (baseType == typeof(System.Exception));
                        baseType = baseType.BaseType;
                    }
                    // No more bases and not Exception-derived, try next type
                    if (!derivedFromException) continue;
                    // We found an Exception-derived type
                    totalExceptionTypes++;
                    // For this Exception-derived type,
                    // reverse the order of the types in the hierarchy
                    String[] h = typeHierarchy.ToString().Split('-');
                    Array.Reverse(h);
                    // Build a new string with the hierarchy in order
                    // from Exception -> Exception-derived type
                    // Add the string to the list of Exception types
                    exceptionTree.Add(String.Join("-", h, 1, h.Length - 1));
                }
            }
            // Sort the Exception types together in order of their hierarchy
            exceptionTree.Sort();
            // Display the Exception tree
            foreach (String s in exceptionTree)
            {
                // For this Exception type, split its base types apart
                string[] x = s.Split('-');
                // Indent based on the number of base types
                // and then show the most-derived type
                Console.WriteLine(new String(' ', 3 * x.Length) + x[x.Length - 1]);
            }
            // Show final status of the types considered
            Console.WriteLine("\n---> of {0} types, {1} are " +
            "derived from System.Exception.",
            totalPublicTypes, totalExceptionTypes);
        }

        private static void LoadAssemblies()
        {
            String[] assemblies = {
"System, 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.Windows.Forms, PublicKeyToken={0}",
"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 Assemblyldentity = String.Format(a, EcmaPublicKeyToken, MSPublicKeyToken) +
                ", Culture=neutral, Version=" + version;
                Assembly.Load(Assemblyldentity);
            }
        }
    }

构建一个Type的实例

一旦你有一个Type对象的引用,你可能想构建这个Type的一个实例。FCL提供了几种机制来做这件事儿:

  • System.Activator的CreateInstance静态方法,Activator类提供了几个CreateInstance方法的重载方法。当调用这个方法时,你或者传递一个Type对象的引用或者传递一个Type对象身份信息的字符串作为参数。
    ObjectHandle是一个允许在一个AppDomain创建对象然后再将该对象传递给另一个AppDomain中,而不需要强制具体化该对象的类型。当你想具体化这个对象,你调用ObjectHandle的Unwrap方法即可。(具体化就是指返回被包装的对象)。
  • System.Activator的CreateInstanceFrom方法。这个方法与上面的方法行为相似,除了它只接受字符串形式的参数。程序集被加载到调用该函数的AppDomain中。
  • System.AppDomain 的方法。本类提供了4个方法来创建一个类型的实例:CreateInstance, CreateInstanceAndUnwrap, CreateInstanceFrom, CreateInstanceFromAndUnwrap这些方法与Activator相应方法的行为相同。
  • System.Type的InvokeMember实例方法。在一个Type对象的实例上,你可以调用InvokeMember方法。这个方法会找到与你传递的参数匹配的构造函数并构造出一个对象。这个对象总是创建在调用AppDomain中,并返回创建的对象的引用。
  • System.Reflection名字空间的ConstructorInfo类的Invoke方法。


注意:CLR不需要值类型定义任何构造函数。所以上面所说的实例化对象的方法中凡与调用构造函数有关的方法在遇到值类型时,会出现问题。然而,Activator类的CreateInstance方法可以创建一个值类型的实例。
(补充,C#中的值类型和引用类型的区别:

结构与类共享大多数相同的语法,但结构比类受到的限制更多:

  • 在结构声明中,除非字段被声明为 const 或 static,否则无法初始化。

  • 结构不能声明默认构造函数(没有参数的构造函数)或析构函数。

  • 结构不能从类或其他结构继承。

  • 结构在赋值时进行复制。将结构赋值给新变量时,将复制所有数据,并且对新副本所做的任何修改不会更改原始副本的数据。

  • 结构是值类型,而类是引用类型。

  • 与类不同,结构的实例化可以不使用 new 运算符。

  • 结构可以声明带参数的构造函数。

  • 一个结构不能从另一个结构或类继承,而且不能作为一个类的基。所有结构都直接继承自 System.ValueType,后者继承自 System.Object

  • 结构可以实现接口。

  • 结构可用作可为 null 的类型,因而可向其赋 null 值。 

 

posted @ 2011-03-12 23:59  王海龙(Heaven)  阅读(531)  评论(0编辑  收藏  举报