25.1 元数据和反射
? 有些程序处理的数据不是数字、文本或图形,而是关于程序和程序类型的信息。
-
有关程序及其类型的数据被称为元数据,保存在程序的程序集中。
-
程序运行时,可以查看其他程序集或其本身的元数据。这种行为叫做反射。
-
要使用反射,必须使用 System.Reflection 命名空间。
25.2 Type 类
? BCL 声明了一个 Type 抽象类(不能有实例),用来包含类型的特征,获取程序使用的类型信息。
? 在运行时,CLR 创建从 Type 派生的类(RuntimeType)的实例,包含类型信息。访问这些实例时,CLR 不会返回派生类的引用,而是返回 Type 类的引用。方便起见,本章将引用指向的对象称为 Type 类型的对象。
- 程序中用到的每一个类型,CLR 都会创建一个包含这个类型信息的 Type 类型对象。
- 同一类型的所有实例只被一个 type 对象关联。
图25.1 对于程序中使用的每个类型,CLR 都会实例化 Type 类型的对象
? 表 25.1 列出了 Type 类中常用的成员。
表25.1 System.Type 类的部分成员
25.3 获取 Type 对象
使用 GetType 方法
? object 类型包含方法 GetType,返回示例的 Type 对象引用。由于每个类型都是由 object 派生的,因此可以在任何类型对象上使用 GetType 方法。
使用 typeof 运算符
? 提供类型名作为操作数,typeof 就会返回 Type 对象的引用。
25.4 什么是特性
? 特性是一种允许向程序集添加元数据的语言结构,用于保存程序结构信息的特殊类型。
- 将应用了特性的程序结构称为目标。
- 设计用来获取和使用元数据的程序称为特性的消费者。
- .NET 预定义了许多特性,也可以自己声明自定义特性。
图25.2 创建和使用特性的相关组件
- 在源代码中将特性应用于程序结构。
- 编译器获取源代码并从特性产生元数据,之后将元数据放到程序集中。
- 消费者程序可以获取特性的元数据以及程序中其他组件的元数据。即,编译器同时生产和消费特性。
- 特性名使用 Pascal 命名法并以 Attribute 后缀结尾。
25.5 应用特性
- 通过在结构前防止特性片段来应用特性。
- 特性片段由方括号包围特性名和参数列表(可有可无)构成。
- 大多数特性只应用于直接跟随在一个或多个特性片段后的结构。
- 引用了特性的结构称为被特性装饰(decorated 或 adorned)。
25.6 预定义的保留特性
25.6.1 Obsolete 特性
? 使用 Obsolete 特性将程序结构标注为“过时”,并可以提供相关的警告信息。
? 程序可以正常运行,但是编译器会产生一条警告信息:
? 另外,可以通过改变第二个参数为 true,将代码标记为错误而不是警告。
25.6.2 Conditional 特性
? Conditional 特性允许包括或排斥指定方法的所有调用,使用该特性时,需要将一个编译符号作为参数。
- 如果定义了编译符号,则编译器会包含所有调用这个方法的代码。
- 如果没有定义编译符号,编译器将忽略代码中这个方法的所有调用。
- 方法本身的 CIL 代码会包含在程序集中,只是调用时会被忽略。
- 除了应用在方法上,Conditional 特性还可以引用在类上,这里不做介绍。
? Conditional 特性应用的方法必须满足以下条件:
- 必须是类或结构体的方法。
- 必须为 void 方法。
- 不能被声明为 override,但可以是 virtual。
- 不能是接口方法的实现。
? 当编译器编译上述代码时,会检查是否定义了编译符号 DoTrace。
- 若定义,则编译器和往常一样包含这些方法的调用。
- 若未定义,则编译器不会输出任何对 TraceMessage 的任何调用代码。
25.6.3 调用者信息特性
? 利用调用者信息特性可以访问文件路径、代码行数、调用成员的名称等源代码信息。
- 这三个特性分别为:
- CallerFilePath。
- CallerLineNumber。
- CallerMemberName。
- 上述特性只能用于方法中的可选参数。
25.6.4 DebuggerStepThrough 特性
? DebuggerStepThrough 特性告诉调试器在执行目标代码时不要进入该方法调试。
- 该特性位于 Sustem.Diagnostics 命名空间。
- 该特性可用于类、结构、构造函数、方法或访问器。
25.6.5 其他预定义特性
表25.2 .NET 中定义的重要特性
25.7 关于应用特性的更多内容
25.7.1 多个特性
? 可以为单个结构应用多个特性。
- 多个特性可以使用下面任何一种格式列出:
- 独立的特性片段。
- 单个特性片段,特性之间使用逗号分隔。
- 可以以任何次序列出特性。
25.7.2 其他类型的目标
? 可以将特性应用到其他程序结构,并可以显示地标注特性。
表25.3 特性目标
25.7.3 全局特性
? 可以使用 assembly 和 module 目标名称来使用显式目标说明符将特性设置在程序集或模块级别。
- 程序集级别的特性必须防止在任何命名空间之外,并且通常放置在 AssemblyInfo.cs 文件中。
- AssemblyInfo.cs 文件通常包含有关公司、产品以及版权信息的元数据。
25.8 自定义特性
? 特性只是一种特殊的类:
- 用户自定义的特性类称为自定义特性。
- 所有特性类都派生自 System.Atrribute。
25.8.1 声明自定义特性
- 声明一个自定义特性,需要做如下工作:
- 声明一个派生自 System.Attribute 的类。
- 起一个以后缀 Attribute 结尾的名称。
- 安全起见,建议声明的特性类为 sealed。
- 由于特性持有目标的信息,所有特性类的公有成员只能是:
25.8.2 使用特性的构造函数
? 每个特性必须至少有一个公共构造函数。
- 和其他类一样,如果不声明构造函数,编译器会产生一个隐式公共无参的构造函数。
- 特性的构造函数和其他构造函数一样,可以被重载。
- 声明构造函数时必须使用类全名,包括后缀。只可以在应用特性时使用短名称。
25.8.3 指定构造函数
? 在为目标应用特性时,其实在指定应该使用哪个构造函数来创建特性实例。
- 应用特性时,构造函数的实参必须在编译时就能确定值。
- 如果应用的特性构造函数没有参数,可以省略圆括号。
25.8.4 使用构造函数
? 和其他类一样,不能显式调用构造函数。特性的实例被创建后,只有特性的消费者访问特性时才能调用构造函数。因此,应用一个特性是一条声明语句,只决定使用哪个构造函数创建特性,而不会当即创建特性。
- 命令语句的意义是:“在这里创建新的类”。
- 声明语句的意义是:“这个特性和这个目标相关联,如果需要创建特性,则使用这个构造函数”。
图25.3 比较构造函数的使用
25.8.5 构造函数中的位置参数和命名参数
? 与普通类的方法和构造函数蕾西,特性的构造函数同样可以使用位置参数和命名参数,且位置参数必须放在命名参数之前。
25.8.6 限制特性的使用
? 使用预定义特性 AttributeUsage 来限制自定义特性的使用范围。
? AttributeUsage 有 3 个重要的公有属性,如表 25.4 所示。
表25.4 AttributeUsage 的公有属性
AttributeUsage 的构造函数
? AttributeUsage 的构造函数接受单个位置参数,该参数设置 ValidOn 属性,指定可使用特性的目标类型。
? 可接受的目标类型是 AttributeTargets 枚举的成员,枚举的所有成员如表 25.5 所示。
表25.5 AttributeTargets 枚举的成员
? 下面示例中的 MyAttribute 只能应用在类上,却不会被应用它的类的派生类继承。
25.8.7 自定义特性的最佳实践
? 建议参考如下示例编写自定义特性:
- 特性类应明确表示目标结构的某种状态。
- 除了属性外,不要实现共有方法或其他函数成员。
- 为了更安全,将特性类声明为 sealed。
- 在特性声明中使用 AttributeUsage 来显式指定特性目标组。
25.9 访问特性
? 使用 Type 对象的方法来获取自定义特性。
25.9.1 使用 IsDefined 方法
- 第一个参数接受需要检查的特性的 Type 对象。
- 第二个参数为 bool 类型,指示是否搜索 MyClass 继承树来查找该特性。
25.9.2 使用 GetCustomAttributes 方法
- 实际返回的对象是 object 数组,因此必须将其强制转换为相应的特性类型。
- 布尔参数指定是否搜索继承树来查找特性。
- 调用 GetCustomAttributes 方法后,每个与目标关联的特性示例就会被创建。