1特性是什么
特性(Attribute)就是写在方括号里的一个标签,放在类、方法这些程序元素的前面。它不改变代码做什么,只给这个元素附加一些信息,这种描述别的数据的信息叫元数据。
| 57 | [DeBugInfo(45, "Zara Ali", "12/8/2012", Message = "Return type mismatch")] | // 第一个标签 |
| 58 | [DeBugInfo(49, "Nuha Ali", "10/10/2012", Message = "Unused variable")] | // 第二个标签 |
| 59 | class Rectangle | // 两个标签都贴在 Rectangle 类前面 |
这里的 DeBugInfo 是后面要自己做的特性。特性会被编译进程序里:编译器认得其中几种并照着办,程序运行时还能用反射(后面会讲)把它们读出来。.NET 里的特性分两种:预定义特性和自定义特性。
视频里用挂牌检验台演示:工件是程序元素,插在上面的卡片是特性,龙门(编译器)从左往右读卡片,认识的就照着办,比如亮灯报警。真实程序里没有卡片,特性是存进程序里的一段元数据。
2语法
[特性名(位置参数, 名字 = 值)]:
- 位置参数:写在前面,必须提供。
- 命名参数:写成“名字 = 值”,可以不写。
上面的例子里,编号、人名、日期是三个位置参数,Message = "..." 是命名参数。整个方括号放在它要修饰的元素前面。
3预定义特性
教程列了三个:AttributeUsage、Conditional、Obsolete。预定义的特性远不止三个,这三个最常用。
Conditional
把一个方法标记成条件方法:括号里的符号定义了,调用才保留。
| 1 | #define DEBUG | // 定义 DEBUG 符号(第 26 节学过) |
| 6 | [Conditional("DEBUG")] | // 只有 DEBUG 定义了,调用 Message 的语句才会被编译 |
| 7 | public static void Message(string msg) | // Message 方法:打印一句话 |
| 8 | { | // Message 开始 |
| 9 | Console.WriteLine(msg); | // 打印 msg |
| 10 | } | // Message 结束 |
查看完整代码(29 行)
| 1 | #define DEBUG | // 定义 DEBUG 符号 |
| 2 | using System; | // 引入 System 命名空间 |
| 3 | using System.Diagnostics; | // Conditional 特性在这个命名空间里 |
| 4 | public 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 类结束 |
| 12 | class 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.
把第一行改成取消定义,再运行,控制台一行都没有打印:
| 1 | #undef DEBUG | // 取消 DEBUG:编译器把三条调用 Message 的语句全部去掉 |
Message 方法本身还在,去掉的只是调用它的语句。括号里写的是符号名,要全部大写,比如 DEBUG、TRACE,不是 Debug、Trace。另外,用调试配置编译时 DEBUG 会自动定义好,不写 #define 也一样。
Obsolete
标记一个不该再用的方法(obsolete 意思是过时的),括号里写一句提示:为什么过时、该改用什么。第二个参数叫 iserror,默认是 false。
| 4 | [Obsolete("Don't use OldMethod, use NewMethod instead")] | // 只写提示,不写第二个参数 |
| 5 | static void OldMethod() | // 过时的方法 |
| 15 | OldMethod(); | // 调用过时方法:警告 CS0618“MyClass.OldMethod()”已过时 |
It is the old method
只给警告,程序照样运行。第二个参数写成 true,编译直接失败:
| 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
专门贴在自定义特性前面,规定这个特性可以怎么用,有三个参数:
validon:能贴在哪些元素上,取值来自AttributeTargets枚举,几个值可以用|组合,不写默认是All。AllowMultiple:同一个元素上能不能贴好几个,默认false。Inherited:派生出来的子类能不能继承这个特性,默认是true(实测见下面)。
| 3 | [AttributeUsage(AttributeTargets.Class | | // 允许贴在类上 |
| 4 | AttributeTargets.Constructor | | // 构造函数上 |
| 5 | AttributeTargets.Field | | // 字段上 |
| 6 | AttributeTargets.Method | | // 方法上 |
| 7 | AttributeTargets.Property, | // 属性上(| 把几个值组合起来) |
| 8 | AllowMultiple = true)] | // 同一个元素上允许贴多个 |
只允许贴在方法上,却贴到了类前面:
| 3 | [AttributeUsage(AttributeTargets.Method)] | // 只允许贴在方法上 |
| 4 | public class DeBugInfo : System.Attribute | // 自定义特性 DeBugInfo |
| 5 | { | // DeBugInfo 类开始 |
| 6 | public DeBugInfo(int bg) { } | // 构造函数:接收一个编号,什么也不做 |
| 7 | } | // DeBugInfo 类结束 |
| 8 | | |
| 9 | [DeBugInfo(45)] | // 贴到了类前面 |
| 10 | class Rectangle | // Rectangle 是类,不是方法 |
Program.cs(9,2): error CS0592: 特性“DeBugInfo”对此声明类型无效。它仅对“方法”声明有效。
去掉 AllowMultiple,Rectangle 前面仍然贴两个 DeBugInfo:
| 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:
| 3 | [AttributeUsage(AttributeTargets.Class)] | // Tag1:没写 Inherited |
| 4 | public class Tag1 : Attribute { } | // 自定义特性 Tag1,类体是空的 |
| 5 | | |
| 6 | [AttributeUsage(AttributeTargets.Class, Inherited = false)] | // Tag2:写明不让继承 |
| 7 | public class Tag2 : Attribute { } | // 自定义特性 Tag2 |
| 9 | [Tag1] | // Shape 贴上 Tag1 |
| 10 | [Tag2] | // Shape 贴上 Tag2 |
| 11 | class Shape { } | // 父类 Shape |
| 12 | | |
| 13 | class Rectangle : Shape { } | // Rectangle 从 Shape 派生,自己没贴特性 |
用反射查 Rectangle 身上有没有这两个特性(typeof( 取到类型 X 的信息):
| 19 | Console.WriteLine(Attribute.IsDefined(typeof(Rectangle), typeof(Tag1))); | // IsDefined:Rectangle 身上有没有 Tag1 |
| 20 | Console.WriteLine(Attribute.IsDefined(typeof(Rectangle), typeof(Tag2))); | // 有没有 Tag2 |
True False
没写的 Tag1 被继承了,说明 Inherited 默认就是 true。
4自定义特性
做一个自定义特性分四步:声明、构建、应用,最后用反射读出来(第四步下一节讲)。
第一步:声明
新特性要从 System. 派生,前面贴上 AttributeUsage(就是上面那段)。示例开头那行注释写的 BugFix,指的就是 DeBugInfo。官方习惯给特性起名以 Attribute 结尾,使用时可以省掉这个结尾。
| 10 | public class DeBugInfo : System.Attribute | // 从 System.Attribute 派生 |
第二步:构建
DeBugInfo 记四样信息:bug 编号、发现它的开发人员、最后一次审查代码的日期、一条消息。前三样必需,通过构造函数传进来(位置参数),每个特性至少要有一个构造函数;消息可选,用能读能写的 Message 属性来接(命名参数)。
这三样存在三个私有字段 bugNo、developer、lastReview 里,外面再用三个只读属性公开出来;消息存在 message 字段里。
| 17 | public DeBugInfo(int bg, string dev, string d) | // 构造函数:三个必需的信息 |
| 18 | { | // 构造函数开始 |
| 19 | this.bugNo = bg; | // 存编号 |
| 20 | this.developer = dev; | // 存开发人员 |
| 21 | this.lastReview = d; | // 存日期 |
| 22 | } | // 构造函数结束 |
第三步:应用
| 69 | [DeBugInfo(55, "Zara Ali", "19/10/2012", | // GetArea 前面贴一个,三个位置参数 |
| 70 | Message = "Return type mismatch")] | // 再加命名参数 Message |
| 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 方法 |
Rectangle 类前面贴了两个 DeBugInfo,这正是 AllowMultiple 允许的。补上一个 Main,创建矩形,调用 Display:
查看完整代码(90 行)
| 1 | using System; | // 引入 System 命名空间 |
| 2 | // 一个自定义特性 BugFix 被赋给类及其成员 | |
| 3 | [AttributeUsage(AttributeTargets.Class | | // 允许贴在类上 |
| 4 | AttributeTargets.Constructor | | // 构造函数上 |
| 5 | AttributeTargets.Field | | // 字段上 |
| 6 | AttributeTargets.Method | | // 方法上 |
| 7 | AttributeTargets.Property, | // 属性上 |
| 8 | AllowMultiple = true)] | // 允许贴多个 |
| 9 | | |
| 10 | public 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")] | // 类前面第二个标签 |
| 59 | class 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 类结束 |
| 83 | class 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 的类)。
5小结
- 特性写在方括号里,放在元素前面,给它附加信息。
Conditional决定调用留不留,Obsolete让用到它的地方警告或报错,AttributeUsage规定自定义特性能贴在哪、能不能贴多个、会不会被继承。- 自定义特性从
System.派生,必需的信息走构造函数,可选的写成命名参数。Attribute
下一节:贴上去的这些信息,程序运行时怎么读出来?
