日期:2010-04-22 浏览次数:20538 次
清晰的组件化目标是否因在库间共享过多类型信息而落空?或许您需要高效的强类型化数据存储,但如果每次对象模型发展后都需要更新您的数据库架构,那会耗费很大成本,所以您更愿意在运行时推断出其类型架构吗?您需要交付能接受任意用户对象的组件,并以某种智能化的方式处理它们吗?您希望库的调方者能以编程方式向您说明它们的类型吗?
如果您发现自己在苦苦维持强类型化数据结构的同时,又冀望于最大化运行时灵活性,那么您大概会愿意考虑反射,以及它如何改善您的软件。在本专栏中,我将探讨 Microsoft .NET Framework 中的 System.Reflection 命名空间,以及它如何为您的开发体验提供助益。我将从一些简单的示例开始,最后将讲述如何处理现实世界中的序列化情形。在此过程中,我会展示反射和 CodeDom 如何配合工作,以有效处理运行时数据。
在深入探究 System.Reflection 之前,我想先讨论一下一般的反射编程。首先,反射可定义为由一个编程系统提供的任何功能,此功能使程序员可以在无需提前了解其标识或正式结构的情况下检查和操作代码实体。这部分内容很多,我将逐一展开说明。
首先,反射提供了什么呢?您能用它做些什么呢?我倾向于将典型的以反射为中心的任务分为两类:检查和操作。检查需要分析对象和类型,以收集有关其定义和行为的结构化信息。除了一些基本规定之外,通常这是在事先不了解它们的情况下进行的。(例如,在 .NET Framework 中,任何东西都继承自 System.Object,并且一个对象类型的引用通常是反射的一般起点。)
操作利用通过检查收集到的信息动态地调用代码,创建已发现类型的新实例,或者甚至可以轻松地动态重新结构化类型和对象。需要指出的一个要点是,对于大多数系统,在运行时操作类型和对象,较之在源代码中静态地进行同等操作,会导致性能降低。由于反射的动态特性,因此这是个必要的取舍,不过有很多技巧和最佳做法可以优化反射的性能。
那么,什么是反射的目标呢?程序员实际检查和操作什么呢?在我对反射的定义中,我用了“代码实体”这个新术语,以强调一个事实:从程序员的角度来说,反射技术有时会使传统对象和类型之间的界限变得模糊。例如,一个典型的以反射为中心的任务可能是:
从对象 O 的句柄开始,并使用反射获得其相关定义(类型 T)的句柄。
检查类型 T,获得它的方法 M 的句柄。
调用另一个对象 O’(同样是类型 T)的方法 M。
请注意,我在从一个实例穿梭到它的底层类型,从这一类型到一个方法,之后又使用此方法的句柄在另一个实例上调用它 — 显然这是在源代码中使用传统的 C# 编程技术无法实现的。在下文中探讨 .NET Framework 的 System.Reflection 之后,我会再次通过一个具体的例子来解释这一情形。
某些编程语言本身可以通过语法提供反射,而另一些平台和框架(如 .NET Framework)则将其作为系统库。不管以何种方式提供反射,在给定情形下使用反射技术的可能性相当复杂。编程系统提供反射的能力取决于诸多因素:程序员很好地利用了编程语言的功能表达了他的概念吗?编译器是否在输出中嵌入足够的结构化信息(元数据),以方便日后的解读?有没有一个运行时子系统或主机解释器来消化这些元数据?平台库是否以对程序员有用的方式,展示此解释结果?
如果您头脑中想象的是一个复杂的、面向对象类型的系统,但在代码中却表现为简单的、C 语言风格的函数,而且没有正式的数据结构,那么显然您的程序不可能动态地推断出,某变量 v1 的指针指向某种类型 T 的对象实例。因为毕竟类型 T 是您头脑中的概念,它从未在您的编程语句中明确地出现。但如果您使用一种更为灵活的面向对象语言(如 C#)来表达程序的抽象结构,并直接引入类型 T 的概念,那么编译器就会把您的想法转换成某种日后可以通过合适的逻辑来理解的形式,就象公共语言运行时 (CLR) 或某种动态语言解释器所提供的一样。
反射完全是动态、运行时的技术吗?简单的说,不是这样。整个开发和执行周期中,很多时候反射对开发人员都可用且有用。一些编程语言通过独立编译器实现,这些编译器将高级代码直接转换成机器能够识别的指令。输出文件只包括编译过的输入,并且运行时没有用于接受不透明对象并动态分析其定义的支持逻辑。这正是许多传统 C 编译器的情形。因为在目标可执行文件中几乎没有支持逻辑,因此您无法完成太多动态反射,然而编译器会不时提供静态反射 — 例如,普遍运用的 typeof 运算符允许程序员在编译时检查类型标识。
另一种完全不同的情况是,解释性编程语言总是通过主进程获得执行(脚本语言通常属于此类)。由于程序的完整定义是可用的(作为输入源代码),并跟完整的语言实现结合在一起(作为解释器本身),因此所有支持自我分析所需的技术都到位了。这种动态语言频繁地提供全面反射功能,以及一组用于动态分析和操作程序的丰富工具。
.NET Framework CLR 和它的承载语言如 C# 属于中间形态。编译器用来把源代码转换成 IL 和元数据,后者与源代码相比虽属于较低级别或者较低“逻辑性”,但仍然保留了很多抽象结构和类型信息。一旦 CLR 启动和承载了此程序,基类库 (BCL) 的 System.Reflection 库便可以使用此信息,并返回关于对象类型、类型成员、成员签名等的信息。此外,它也可以支持调用,包括后期绑定调用。
.NET 中的反射
要在用 .NET Framework 编程时利用反射,您可以使用 System.Reflection 命名空间。此命名空间提供封装了很多运行时概念的类,例如程序集、模块、类型、方法、构造函数、字段和属性。图 1 中的表显示,System.Reflection 中的类如何与概念上运行时的对应项对应起来。
尽管很重要,不过 System.Reflection.Assembly 和 System.Reflection.Module 主要用于定位新代码并将其加载到运行时。本专栏中,我暂不讨论这些部分,并且假定所有相关代码都已经加载。
要检查和操作已加载代码,典型模式主要是 System.Type。通常,您从获得一个所关注运行时类别的 System.Type 实例开始(通过 Object.GetType)。接着您可以使用 System.Type 的各种方法,在 System.Reflection 中探索类型的定义并获得其它类的实例。例如,如果您对某特定方法感兴趣,并希望获得此方法的一个 System.Reflection.MethodInfo 实例(可能通过 Type.GetMethod)。同样,如果您对某字段感兴趣,并希望获得此字段的一个 System.Reflection.FieldInfo 实例(可能通过 Type.GetField)。
一旦获得所有必要的反射实例对象,即可根据需要遵循检查或操作的步骤继续。检查时,您在反射类中使用各种描述性属性,获得您需要的信息(这是通用类型吗?这是实例方法吗?)。操作时,您可以动态地调用并执行方法,通过调用构造函数创建新对象,等等。
检查类型和成员
让我们跳转到一些代码中,探索如何运用基本反射进行检查。我将集中讨论类型分析。从一个对象开始,我将检索它的类型,而后考察几个有意思的成员。
首先需要注意的是,在类定义中,乍看起来说明方法的篇幅比我预期的要多很多。这些额外的方法是从哪里来的呢?任何精通 .NET Framework 对象层次结构的人,都会识别从通用基类 Object 自身继承的这些方法。(事实上,我首先使用了 Object.GetType 检索其类型。)此外,您可以看到属性的 getter 函数。现在,如果您只需要 MyClass 自身显式定义的函数,该怎么办呢?换句话说,您如何隐藏继承的函数?或者您可能只需要显式定义的实例函数?
随便在线看看 MSDN,就会发现大家都愿意使用 GetMethods 第二个重载方法,它接受 BindingFlags 参数。通过结合来自 BindingFlags 枚举中不同的值,您可以让函数仅返回所需的方法子集。替换 GetMethods 调用,代之以:
GetMethods(BindingFlags.Instance | BindingFlags.DeclaredOnly |BindingFlags.Public)
结果是,您得到以下输出(注意这里不存在静态帮助器函数和继承自 System.Object 的函数)。