导航
第十六章 Reflection, Metadata and Dynamic Programming
16.1 在运行期间检查代码和动态编程 326
本章主要关注自定义特性(custom attributes),反射(Reflection)和动态编程(Dynamic Programming)。自定义特性是一种机制,它让你可以为程序里的元素关联一些自定义的元数据(metadata)。元数据是在编译时创建的,并且嵌入到程序集中。反射(Reflection)则是一个专用术语(generic term),用来描述在运行时检测(inspect)或者操控(manipulate)程序内部元素的一种能力(capability)。举个例子,通过反射你可以:
- 枚举某种类型的所有成员。
- 实例化一个新的对象。
- 执行一个对象里的指定成员。
- 查找某个类型的详细信息。
- 查找程序集的详细信息。
- 检查某种类型应用了什么自定义特性。
- 动态地创建和编译一个新的程序集。
上面列举的内容代表了一大部分功能(functionality),包含了.NET基础类库提供的最强大和最复杂的部分。因为一个章节并不能完整地介绍反射的所有内容,我会着重介绍那些你可能会经常用到的。
为了演示自定义特性和反射,在本章中,你首先开发了一个公司案例,定期发布软件更新(ships upgrades),并希望每一次更新内容都能被自动记录(documented automatically)。于是,你定义了一些自定义特性(custom attributes),用来标识程序内容最后一次更改是什么时候,以及它们都改动了什么。然后你利用反射,开发了一个应用程序,在程序集里查找这些事先定义好的特性,这样你就能自动显示某个日期之后软件进行过的更新的所有细节。
本章中另外一个例子主要研究的是一个读写数据库的应用,我们用自定义特性来标记某个类和属性分别与数据库中哪张表和数据列一一对应。运行时(runtime)通过从程序集里读取这些特性,程序可以自动读取或者将数据写入到数据库合适的位置上,而不需要为每张数据表或数据列另外编写特定的逻辑。
本章关注的第二大部分是动态编程,从C# 4.0开始加入的特性,通过dynamic关键字来使用。一些快速发展的语言,如Ruby和Python,还有Javascript,都加强了动态编程部分的功能。虽然C#仍然是一门静态类型为主的语言,增加对动态编程的支持会让C#更为广大开发者所用。使用动态语言特性允许你在C#中调用脚本函数(allows for calling script functions from within C#)。
在本章中,你将会看到dynamic类型的规则以及如何使用它。你也会看到DynamicObject的实现并且学会如何使用。我们也会提到Framework对DynamicObject的内部实现,这个叫ExpandoObject。
16.2 自定义特性 327
在本书前面的内容中,你可能已经见过如何在程序中为各种各样的Item定义特性(attributes)。这些特性被微软定义成.NET的一部分,并且他们大部分都能获得C#编译器的特殊支持。这意味着,通过那些特殊指定的特性,你可以自定义编译器的整个编译过程——例如:你可以通过定义StructLayout特性来安排一个struct在内存中如何存储。
.NET还允许你定义你自己独有的特性。显而易见的是,这些特性不会影响到任何编译过程因为编译器无法识别它们(has no intrinsic awareness)。然而当它们应用到程序元素(applied to program elements)上时,这些特性会被当做元数据嵌入到编译好的程序集中。
有一说一,元数据可能对文档方面很有用,但它最强大的地方是通过反射技术,让你的代码可以在运行时,随心所欲地获取这些元数据并进一步地处理。这意味着你定义的特性将会影响你代码的运行方式。例如,自定义特性允许为某些自定义权限类(custom permission classes)使用声明式代码检查可访问性(enable declarative code access security checks),将程序元素之间的信息关联起来方便让测试工具调用,或者当开发扩展框架时作为一个附件或者模块进行加载。
16.2.1 编写自定义特性 327
了解编译器遇到一个特性时是如何应用的,将会对你理解如何编写属于你自己的自定义特性很有帮助。以数据库的例子为例,假定你定义了一个C#属性,像下面这样子:
[FieldName("SocialSecurityNumber")]
public string SocialSecurityNumber
{
get {
//...
}
}
当C#编译器识别到这个属性拥有一个特性(FieldName),首先它会为这个特性名称添加一个Attribute后缀,这个特性在编译器里的全名为:FieldNameAttribute。然后编译器在它加载的所有命名空间(using语句加载的那些)中中搜索这个全名,看看是否有某个类与这个名称一致。注意如果你的特性名是以Attribute结尾的话,编译器不会再次给你追加一个Attribute的后缀,它会保持特性名不变。因此前面的代码与下面等价:
[FieldNameAttribute("SocialSecurityNumber")]
public string SocialSecurityNumber
{
get {
//...
}
}
编译器假定(expect)会找到一个相应的特性类,并且这个特性类是直接或者间接派生自System.Attribute的。编译器同样假定这个类包含了特性使用的相关信息,尤其需要以下相关信息:
- 程序内什么元素(classes,structs,properties,methods还是其他)允许应用这种特性。
- 同类元素是否允许多次应用同一特性。
- 当特性应用在类或者接口上时,是否会被子类或者派生接口继承。
- 特性需要的必须(mandatory)和可选的参数。
如果编译器没有找到相应的特性类,或者它能找到一个同名类,但该类不满足特性类的所有特征,编译器会报一个编译错误。例如,如果特性类指定了该特性只能应用在一个class上,而你却在struct上声明了该特性,此时就会发生编译错误。
让我们接着上面的例子往下看,假定你定义的FieldName特性如下所示:
[AttributeUsage(AttributeTargets.Property, AllowMultiple=false, Inherited=false)]
public class FieldNameAttribute: Attribute
{
private string _name;
public FieldNameAttribute(string name)
{
_name = name;
}
}
指定Attribute的属性值
首先我们要注意的是一个特性类,它自己就被一个特性标记——System.AttributeUsage特性,这个特性是Microsoft定义的,由C#编译器提供特殊支持。(你可能会争辩说AttributeUsage根本就不是一个Attribute,它更像一个元数据,因为它只能被特定的Attributes类使用,而其他的普通类无法使用。)AttributeUsage的主要目的是用来区分你自定义的特性可以用在程序元素的类型上。这部分内容由AttributeUsage的第一个参数提供,这是一个必须的枚举类型参数——AttributeTargets。在前面的例子中,你可以指定FieldName特性仅仅允许用在Properties上,这没什么问题,因为最早的代码片段里我们就是这么使用的。AttributeTargets的枚举值如下所示:
- All
- Assembly
- Class
- Constructor
- Delegate
- Enum
- Event
- Field
- GenericParameter
- Interface
- Method
- Module
- Parameter
- Property
- ReturnValue
- Struct
上面列举了所有你能使用特性的程序元素。注意当你为某个程序元素应用特性时,你需要在这个元素前使用中括号[]
,将特性写在中括号内。然而,上面里有两个值并没有对应任何程序元素:Assembly和Module。假如一个特性允许在整个程序集或者整个模块内使用,与其挨个定义所有的程序元素,不如用一个全体定义来代表他们。这种情况下,你可以将这个特性写在你代码的任何位置,但是它必须用Assembly或者Module关键字作为前缀:
[assembly:SomeAssemblyAttribute(Parameters)]
[module:SomeAssemblyAttribute(Parameters)]
当你需要为你的自定义特性指定某几个有效的目标元素时,你可以使用|
操作符将它们合并起来。例如,你想指定你的FieldName可以应用在Fields和Properties上的话,你可以这么写:
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple=false, Inherited=false)]
public class FieldNameAttribute: Attribute