第 30 节 · 第四部分 进阶特性

特性(Attribute)

特性是写在元素前面方括号里的附加信息(元数据);编译器认得 Conditional、Obsolete、AttributeUsage 这几种并照着办,自定义特性只是存进程序,要等运行时用反射读。

时长 8:28授课计划第 12 周

学完这一节,你能

讲义

跟着视频的顺序写。每行代码都有注释,代码和运行结果都是实际运行得到的。

1特性是什么

特性(Attribute)就是写在方括号里的一个标签,放在类、方法这些程序元素的前面。它不改变代码做什么,只给这个元素附加一些信息,这种描述别的数据的信息叫元数据。

C# · 第 57-59 行
57[DeBugInfo(45, "Zara Ali", "12/8/2012", Message = "Return type mismatch")]// 第一个标签
58[DeBugInfo(49, "Nuha Ali", "10/10/2012", Message = "Unused variable")]// 第二个标签
59class Rectangle// 两个标签都贴在 Rectangle 类前面

这里的 DeBugInfo 是后面要自己做的特性。特性会被编译进程序里:编译器认得其中几种并照着办,程序运行时还能用反射(后面会讲)把它们读出来。.NET 里的特性分两种:预定义特性和自定义特性。

视频里用挂牌检验台演示:工件是程序元素,插在上面的卡片是特性,龙门(编译器)从左往右读卡片,认识的就照着办,比如亮灯报警。真实程序里没有卡片,特性是存进程序里的一段元数据。

2语法

[特性名(位置参数, 名字 = 值)]:

  • 位置参数:写在前面,必须提供。
  • 命名参数:写成“名字 = 值”,可以不写。

上面的例子里,编号、人名、日期是三个位置参数,Message = "..." 是命名参数。整个方括号放在它要修饰的元素前面。

3预定义特性

教程列了三个:AttributeUsage、Conditional、Obsolete。预定义的特性远不止三个,这三个最常用。

Conditional

把一个方法标记成条件方法:括号里的符号定义了,调用才保留。

C# · 第 1 行
1#define DEBUG// 定义 DEBUG 符号(第 26 节学过)
C# · 第 6-10 行
6[Conditional("DEBUG")]// 只有 DEBUG 定义了,调用 Message 的语句才会被编译
7public static void Message(string msg)// Message 方法:打印一句话
8{// Message 开始
9 Console.WriteLine(msg);// 打印 msg
10}// Message 结束
查看完整代码(29 行)
C#
1#define DEBUG// 定义 DEBUG 符号
2using System;// 引入 System 命名空间
3using System.Diagnostics;// Conditional 特性在这个命名空间里
4public class Myclass// 定义 Myclass 类
5{// Myclass 类开始
6 [Conditional("DEBUG")]// 条件方法:看 DEBUG 有没有定义
7 public static void Message(string msg)// Message 方法
8 {// Message 开始
9 Console.WriteLine(msg);// 打印 msg
10 }// Message 结束
11}// Myclass 类结束
12class Test// 定义 Test 类
13{// Test 类开始
14 static void function1()// function1 方法
15 {// function1 开始
16 Myclass.Message("In Function 1.");// 第 2 次调用 Message
17 function2();// 再调用 function2
18 }// function1 结束
19 static void function2()// function2 方法
20 {// function2 开始
21 Myclass.Message("In Function 2.");// 第 3 次调用 Message
22 }// function2 结束
23 public static void Main()// 程序入口 Main 方法
24 {// Main 开始
25 Myclass.Message("In Main function.");// 第 1 次调用 Message
26 function1();// 调用 function1
27 Console.ReadKey();// 等待按键(现在的 VS 不写也会停住)
28 }// Main 结束
29}// Test 类结束
运行结果
In Main function.
In Function 1.
In Function 2.

把第一行改成取消定义,再运行,控制台一行都没有打印:

C# · 第 1 行
1#undef DEBUG// 取消 DEBUG:编译器把三条调用 Message 的语句全部去掉

Message 方法本身还在,去掉的只是调用它的语句。括号里写的是符号名,要全部大写,比如 DEBUG、TRACE,不是 Debug、Trace。另外,用调试配置编译时 DEBUG 会自动定义好,不写 #define 也一样。

Obsolete

标记一个不该再用的方法(obsolete 意思是过时的),括号里写一句提示:为什么过时、该改用什么。第二个参数叫 iserror,默认是 false。

C# · 第 4-5 行
4[Obsolete("Don't use OldMethod, use NewMethod instead")]// 只写提示,不写第二个参数
5static void OldMethod()// 过时的方法
C# · 第 15 行
15OldMethod();// 调用过时方法:警告 CS0618“MyClass.OldMethod()”已过时
运行结果
It is the old method

只给警告,程序照样运行。第二个参数写成 true,编译直接失败:

C# · 第 4 行
4[Obsolete("Don't use OldMethod, use NewMethod instead", true)]// 第二个参数 true:用到它就报错
编译报错
Program.cs(15,7): error CS0619: “MyClass.OldMethod()”已过时:“Don't use OldMethod, use NewMethod instead”

报错写明了是哪个方法过时。Obsolete 不会让编译器丢掉这个方法,方法还在,只是用到它的地方会收到警告或报错。

AttributeUsage

专门贴在自定义特性前面,规定这个特性可以怎么用,有三个参数:

  1. validon:能贴在哪些元素上,取值来自 AttributeTargets 枚举,几个值可以用 | 组合,不写默认是 All。
  2. AllowMultiple:同一个元素上能不能贴好几个,默认 false。
  3. Inherited:派生出来的子类能不能继承这个特性,默认是 true(实测见下面)。
C# · 第 3-8 行
3[AttributeUsage(AttributeTargets.Class |// 允许贴在类上
4AttributeTargets.Constructor |// 构造函数上
5AttributeTargets.Field |// 字段上
6AttributeTargets.Method |// 方法上
7AttributeTargets.Property,// 属性上(| 把几个值组合起来)
8AllowMultiple = true)]// 同一个元素上允许贴多个

只允许贴在方法上,却贴到了类前面:

C# · 第 3-10 行
3[AttributeUsage(AttributeTargets.Method)]// 只允许贴在方法上
4public class DeBugInfo : System.Attribute// 自定义特性 DeBugInfo
5{// DeBugInfo 类开始
6 public DeBugInfo(int bg) { }// 构造函数:接收一个编号,什么也不做
7}// DeBugInfo 类结束
8
9[DeBugInfo(45)]// 贴到了类前面
10class Rectangle// Rectangle 是类,不是方法
编译报错
Program.cs(9,2): error CS0592: 特性“DeBugInfo”对此声明类型无效。它仅对“方法”声明有效。

去掉 AllowMultiple,Rectangle 前面仍然贴两个 DeBugInfo:

C# · 第 56-57 行
56[DeBugInfo(45, "Zara Ali", "12/8/2012", Message = "Return type mismatch")]// 第一个
57[DeBugInfo(49, "Nuha Ali", "10/10/2012", Message = "Unused variable")]// 第二个:不允许贴多个
编译报错
Program.cs(57,2): error CS0579: “DeBugInfo”特性重复

最后看 Inherited:

C# · 第 3-7 行
3[AttributeUsage(AttributeTargets.Class)]// Tag1:没写 Inherited
4public class Tag1 : Attribute { }// 自定义特性 Tag1,类体是空的
5
6[AttributeUsage(AttributeTargets.Class, Inherited = false)]// Tag2:写明不让继承
7public class Tag2 : Attribute { }// 自定义特性 Tag2
C# · 第 9-13 行
9[Tag1]// Shape 贴上 Tag1
10[Tag2]// Shape 贴上 Tag2
11class Shape { }// 父类 Shape
12
13class Rectangle : Shape { }// Rectangle 从 Shape 派生,自己没贴特性

用反射查 Rectangle 身上有没有这两个特性(typeof(X) 取到类型 X 的信息):

C# · 第 19-20 行
19Console.WriteLine(Attribute.IsDefined(typeof(Rectangle), typeof(Tag1)));// IsDefined:Rectangle 身上有没有 Tag1
20Console.WriteLine(Attribute.IsDefined(typeof(Rectangle), typeof(Tag2)));// 有没有 Tag2
运行结果
True
False

没写的 Tag1 被继承了,说明 Inherited 默认就是 true。

4自定义特性

做一个自定义特性分四步:声明、构建、应用,最后用反射读出来(第四步下一节讲)。

第一步:声明

新特性要从 System.Attribute 派生,前面贴上 AttributeUsage(就是上面那段)。示例开头那行注释写的 BugFix,指的就是 DeBugInfo。官方习惯给特性起名以 Attribute 结尾,使用时可以省掉这个结尾。

C# · 第 10 行
10public class DeBugInfo : System.Attribute// 从 System.Attribute 派生

第二步:构建

DeBugInfo 记四样信息:bug 编号、发现它的开发人员、最后一次审查代码的日期、一条消息。前三样必需,通过构造函数传进来(位置参数),每个特性至少要有一个构造函数;消息可选,用能读能写的 Message 属性来接(命名参数)。

这三样存在三个私有字段 bugNo、developer、lastReview 里,外面再用三个只读属性公开出来;消息存在 message 字段里。

C# · 第 17-22 行
17public DeBugInfo(int bg, string dev, string d)// 构造函数:三个必需的信息
18{// 构造函数开始
19 this.bugNo = bg;// 存编号
20 this.developer = dev;// 存开发人员
21 this.lastReview = d;// 存日期
22}// 构造函数结束

第三步:应用

C# · 第 69-76 行
69[DeBugInfo(55, "Zara Ali", "19/10/2012",// GetArea 前面贴一个,三个位置参数
70Message = "Return type mismatch")]// 再加命名参数 Message
71public double GetArea()// GetArea 方法
72{// GetArea 开始
73 return length * width;// 返回长 × 宽
74}// GetArea 结束
75[DeBugInfo(56, "Zara Ali", "19/10/2012")]// Display 前面也贴一个,没写消息
76public void Display()// Display 方法

Rectangle 类前面贴了两个 DeBugInfo,这正是 AllowMultiple 允许的。补上一个 Main,创建矩形,调用 Display:

查看完整代码(90 行)
C#
1using System;// 引入 System 命名空间
2// 一个自定义特性 BugFix 被赋给类及其成员
3[AttributeUsage(AttributeTargets.Class |// 允许贴在类上
4AttributeTargets.Constructor |// 构造函数上
5AttributeTargets.Field |// 字段上
6AttributeTargets.Method |// 方法上
7AttributeTargets.Property,// 属性上
8AllowMultiple = true)]// 允许贴多个
9
10public class DeBugInfo : System.Attribute// 自定义特性 DeBugInfo
11{// DeBugInfo 类开始
12 private int bugNo;// 私有字段:bug 编号
13 private string developer;// 私有字段:开发人员
14 private string lastReview;// 私有字段:最后审查日期
15 public string message;// 消息
16
17 public DeBugInfo(int bg, string dev, string d)// 构造函数:三个位置参数
18 {// 构造函数开始
19 this.bugNo = bg;// 存编号
20 this.developer = dev;// 存开发人员
21 this.lastReview = d;// 存日期
22 }// 构造函数结束
23
24 public int BugNo// 只读属性 BugNo
25 {// BugNo 开始
26 get// 只有 get,没有 set
27 {// get 开始
28 return bugNo;// 返回编号
29 }// get 结束
30 }// BugNo 结束
31 public string Developer// 只读属性 Developer
32 {// Developer 开始
33 get// 只有 get
34 {// get 开始
35 return developer;// 返回开发人员
36 }// get 结束
37 }// Developer 结束
38 public string LastReview// 只读属性 LastReview
39 {// LastReview 开始
40 get// 只有 get
41 {// get 开始
42 return lastReview;// 返回日期
43 }// get 结束
44 }// LastReview 结束
45 public string Message// 能读能写的属性 Message,用作命名参数
46 {// Message 开始
47 get// 读
48 {// get 开始
49 return message;// 返回消息
50 }// get 结束
51 set// 写
52 {// set 开始
53 message = value;// value 是赋进来的值
54 }// set 结束
55 }// Message 结束
56}// DeBugInfo 类结束
57[DeBugInfo(45, "Zara Ali", "12/8/2012", Message = "Return type mismatch")]// 类前面第一个标签
58[DeBugInfo(49, "Nuha Ali", "10/10/2012", Message = "Unused variable")]// 类前面第二个标签
59class Rectangle// 定义 Rectangle 类
60{// Rectangle 类开始
61 // 成员变量
62 protected double length;// 长
63 protected double width;// 宽
64 public Rectangle(double l, double w)// 构造函数
65 {// 构造函数开始
66 length = l;// 存长
67 width = w;// 存宽
68 }// 构造函数结束
69 [DeBugInfo(55, "Zara Ali", "19/10/2012",// GetArea 的标签
70 Message = "Return type mismatch")]// 带消息
71 public double GetArea()// GetArea 方法
72 {// GetArea 开始
73 return length * width;// 返回面积
74 }// GetArea 结束
75 [DeBugInfo(56, "Zara Ali", "19/10/2012")]// Display 的标签,没写消息
76 public void Display()// Display 方法
77 {// Display 开始
78 Console.WriteLine("Length: {0}", length);// 打印长
79 Console.WriteLine("Width: {0}", width);// 打印宽
80 Console.WriteLine("Area: {0}", GetArea());// 打印面积
81 }// Display 结束
82}// Rectangle 类结束
83class ExecuteRectangle// 补上的 ExecuteRectangle 类
84{// ExecuteRectangle 类开始
85 static void Main(string[] args)// 程序入口 Main 方法
86 {// Main 开始
87 Rectangle r = new Rectangle(4.5, 7.5);// 创建 4.5 × 7.5 的矩形
88 r.Display();// 调用 Display
89 }// Main 结束
90}// ExecuteRectangle 类结束
运行结果
Length: 4.5
Width: 7.5
Area: 33.75

贴了这么多特性,输出和没贴时一模一样:编译器不认识 DeBugInfo,读到它什么也不做,只把信息存进程序里。下一节读这些信息用的是 System.Reflection 命名空间里的类型(没有一个叫 Reflection 的类)。

5小结

  • 特性写在方括号里,放在元素前面,给它附加信息。
  • Conditional 决定调用留不留,Obsolete 让用到它的地方警告或报错,AttributeUsage 规定自定义特性能贴在哪、能不能贴多个、会不会被继承。
  • 自定义特性从 System.Attribute 派生,必需的信息走构造函数,可选的写成命名参数。

下一节:贴上去的这些信息,程序运行时怎么读出来?

检查题

5 道,选完马上告诉你对不对。

  1. 1视频里的 Conditional 例子把第一行改成 #undef DEBUG,输出?

  2. 2[Obsolete("…")](不写第二个参数)的方法被调用,结果?

  3. 3AttributeUsage 只写 AttributeTargets.Method,却把这个特性贴在类前面?

  4. 4Shape 贴了不写 Inherited 的 Tag1,Rectangle 派生自 Shape,Rectangle 有没有 Tag1?

  5. 5[DeBugInfo(45, "Zara Ali", "12/8/2012", Message = "…")] 里哪个是命名参数?