思考并回答以下问题:
- 特性的作用是什么?
- 自定义特性要用起来就必须能被识别,如何做到?
- 如何用反射在运行时实现动态绑定?如何在编译时利用一个成员的名称来调用该成员?
- 在执行时调用目标未知的情况下,也要用到反射。怎么理解?
本章涵盖:
- 反射
- 使用System.Type访问元数据
- 成员调用
- 泛型类型上的反射
- 特性
- 自定义特性
- 查找特性
- 使用构造器来初始化特性
- System.AttributeUsageAttribute
- 命名参数
特性(attribute)的作用是在程序集中插入额外的元数据,并将元数据同编程构造(比如类、方法或者属性)关联起来。本章探讨了特性的细节,并描述了如何创建自定义特性。自定义特性要用起来就必须能被识别。这是通过反射(reflection)来实现的。本章首先讨论了反射,其中包括如何用它在运行时实现动态绑定,以及如何在编译时利用一个成员的名称来调用该成员。在像代码生成器这样的工具中,会频繁地执行这个操作。除此之外,在执行时调用目标未知的情况下,也要用到反射。
反射
可以利用反射做下面这些事情。
- 访问程序集中类型的元数据。其中包括像完整类型名和成员名这样的构造,以及对一个构造进行修饰的任何特性。
- 使用元数据,在运行时动态调用一个类型的成员,而不是执行编译时绑定。
反射是指对程序集中的元数据进行检查的过程。在以前,当代码编译成一种机器语言时,关于代码的所有元数据(比如类型和方法名)都会被丢弃。相反,当C#编译成CIL时,它会维持关于代码的大部分元数据。除此之外,可以利用反射枚举程序集中的所有类型,找出满足特定条件的那些。我们通过System.Type的实例来访问类型的元数据,该对象包含了对类型实例的成员进行枚举的方法。除此之外,可在被检查类型的特定对象上调用那些成员。
基于反射,人们发展出了一系列前所未有的编程模式。例如,反射允许枚举程序集中的所有类型及其成员。在这个过程中,可以创建对程序集API进行编档所需的存根(stub)。然后,可将通过反射获取的元数据与通过XML注释(使用/doc开关)创建的XML文档合并,从而创建API文档。类似地,程序员可以利用反射元数据来生成代码,从而将业务对象(business object)持久化(序列化)到数据库中。可以在显示对象集合的列表控件中使用反射。基于这个集合,列表控件可以利用反射来遍历集合中的一个对象的所有属性,并在列表中为每个属性都定义一个列。除此之外,通过调用每个对象的每个属性,列表控件可以使用对象中包含的数据来填充每一行和每一列——即使对象的数据类型在编译时是未知的。
.NET Framework所提供的XmlSerializer、ValueType和DataBinder类均在其实现中利用了反射技术。
使用System.Type访问元数据
读取类型的元数据,关键在于获得System.Type的一个实例,它代表了目标类型实例。System.Type提供了获取类型信息的所有方法。可以用它回答以下问题。
- 类型的名称是什么(Type.Name)?
- 类型是public的吗(Type.IsPublic)?
- 类型的基类型是什么(Type.BaseType)?
- 类型支持任何接口吗(Type.GetInterfaces())?
- 类型是在哪个程序集中定义的(Type.Assembly)?
- 类型的属性、方法、字段是什么(Type.GetProperties()、Type.GetMethods()、Тype.GetFields())?
- 都有什么特性在修饰一个类型(Type.GetCustomAttributes())?
还有其他成员未能一列出,但总而言之,它们都提供了与特定类型有关的信息。很明显,现在的关键是获得对类型的Type对象的引用。我们主要通过object.GetType()和typeof()来达到这个目的。
注意
GetMethods()调用不能返回扩展方法,扩展方法只能作为实现类型的静态成员使用。
1.GetType()
object包含一个GetType()成员。因此,所有类型都包含该方法。调用GetType()可获得与原始对象对应的System.Type实例。代码清单1对此进行了演示,它使用来自DateTime的一个Type实例。输出1展示了结果。
代码清单1 使用Type.GetProperties()获取对象的public属性
1 | DateTime dateTime = new DateTime(); |
输出1
1 | Date |
在调用了GetType()之后,程序遍历从Type.GetProperties()返回的每个System.Reflection.PropertyInfo实例,并显示属性名。成功调用GetType()的关键在于获得一个对象实例。但在某些时候,这样的实例是无法获得的。例如,静态类是无法实例化的,所以没办法调用GetType()。
2.typeof()
获得Type对象的另一个办法是使用typeof表达式。typeof在编译时绑定到特定的Type实例,并直接获取类型作为参数。代码清单2演示了如何同时使用Enum.Parse()和typeof。
代码清单2使用typeof()创建System.Type实例
1 | using System.Diagnostics; |
Enum.Parse()获取标识了一个枚举的Type对象,然后将一个字符串转换成特定的枚举值。在本例中,它将“Idle”转换成System.Diagnostics.ThreadPriorityLevel.Idle。
成员调用
反射并非仅可以用来获取元数据。下一步是获取元数据,并动态调用它引用的成员。假定现在定义一个类,代表一个应用程序的命令行,并把它命名为CommandLineInfo。对于这个类来说,最困难的地方在于如何在类中填充启动应用程序时的实际命令行数据。然而,利用反射,可以将命令行选项映射到属性名,并在运行时动态设置属性。代码清单3对此进行了演示。
代码清单3 动态调用成员
1 | namespace AddisonWesley.Michaelis.EssentialCSharp.Chapter17.Listing17_03 |
虽然这是一个较长的程序,但代码结构是相当简单的。Main()首先实例化一个CommandLineInfo类。这个类型专门用来包含当前这个程序的命令行数据。每个属性都对应于程序的一个命令行选项,具体的命令行如输出2所示。
输出2
1 | Compress.exe /Out: <file name> /Help |
CommandLineInfo对象被传给CommandLineHandler的TryParse()方法。该方法首先枚举每个选项,并分离出选项名(比如Help或Out)。在确定了名称之后,代码在CommandLineInfo对象上执行反射,查找同名的一个实例属性。如果找到这样的属性,就通过一个SetValue()调用,并指定与属性类型对应的数据,从而完成对属性的赋值。SetValue()的参数包括要设置值的对象、新值以及一个额外的index参数(除非属性是一个索引器,否则该参数就为null)。上述代码能处理3种属性类型:bool、string和枚举。在枚举的情况下,我们要解析选项值,并将文本的枚举等价值赋给属性。如果TryParse()调用成功,方法会退出,而CommandLineInfo对象会使用来自命令行的数据进行初始化。
有趣的是,虽然commandLineinfo是嵌套在Program中的一个private类,但CommandLineHandler在它上面执行反射没有任何问题,甚至可以调用它的成员。换言之,只要设置了恰当的代码访问安全性(code access security, CAS)权限,反射就可以绕过可访问性规则。例如,假定Out是private的,TryParse()方法仍然可以向其赋值。由于这一点,所以可以将CommandLineHandler转移到一个单独的程序集中,并在多个程序之间共享它,而且每个程序都有它们自己的CommandLineInfo类。
在这个特定的例子中,是用PropertyInfo.SetValue()来调用CommandLineInfo的一个成员。此外,PropertyInfo还包含一个GetValue()方法,可以用它从属性中获取数据。然而,对于方法,有一个MethodInfo类可供利用,该类提供了一个Invoke()成员。MethodInfo和PropertyInfo都是从MemberInfo继承的(虽然并非直接),如图1所示。
在这个例子中,之所以设置CAS权限来允许私有成员调用,是因为程序是从本地电脑中运行的。默认情况下,本地安装的程序是受信任区域的一部分,已被授予了恰当的权限。然而,从一个远程位置运行的程序需要被显式授予这样的权限。