【C#特性】 Attribute 特性
msdn:https://docs.microsoft.com/zh-cn/dotnet/csharp/programming-guide/concepts/attributes/creating-custom-attributes
目录:
Attribute与Property 的翻译区别
Attribute 是什么
Attribute 的命名约定
Attribute 的作用
Attribute作为编译器的指令
Attribute运用范围
Attribute使用场景
Attribute 与注释的区别
Attribute 的本质
Attribute 实例化有什么独特之处呢?
预定义Attribute
自定义Attribute
说到他们之间的区别,就必须说到亚里士多德的《本体论》了。
property :属性, 属|种的特点(第二实体)。
Property 是什么意思?
property表示属性,即属固有的特性。比如:
”人“是属,人固有的特性是,姓名、性别、年龄。
“小明”是第一实体, ”人“是第二实体,人是小明这个体的属。小明自然继承”人“属所固有的特性,即小明也具有姓名、性别、年龄特性。对于小明这个个体来说,姓名、性别、年龄是属所具有的特性,而不是小明这个个体本身的特性,因此它们叫属性。这种是相对关系的取名方式。
“小明”要获得属性,就必须通过getter 、和sett方法获得“人”的特性(如何 :姓名、性别、年龄特性)。
Attribute表示特性,即某个实体所独自拥有的特性。
Attribute 本质是一个类,可以用来修饰各种需要被修饰的目标。
简单的说,Attribute就是一种“附着物” —— 就像牡蛎吸附在船底或礁石上一样。
C#
语言中,特性是元数据,附加于字段或代码块,如程序集(assemblies
)、成员变量、数据类型。编译器与反射式编程可访问特性。开发者可以决定把特性作为元数据,专门用于表示与给定应用程序,类和成员有关的,与实例无关的各类信息。开发者也可以决定把一些特性暴露为属性(properties),用作更大的应用程序框架的一部分。特性可以实现为类(派生自System.Attribute
)。特性是对现有代码的一种扩展,本身并不会影响既有的逻辑,除非主动去获取,使用特性里实现的功能。
这是.Net的一个约定,所有的特性应该均以Attribute来结尾,在为对象标记特性时如果没有添加Attribute,编译器会自动寻找带有Attribute的版本。
[Obsolete("请使用新的SendMsg(Message msg)重载方法")] public static void ShowMsg()=>Console.WriteLine("这是旧的SendMsg()方法");
实际上,当你用鼠标框选住Obsolete,然后按下F12转到定义,会发现它的全名是ObsoleteAttribute,继承自Attribute类。
特性Attribute 的作用是添加元数据。
元数据可以被工具支持,比如:编译器用元数据来辅助编译,调试器用元数据来调试程序。
C#的attribute会被编译成元数据,反射的时候就可以提取附着在方法、属性、字段上的attribute 然后做相应的操作。
在C#中存在着一定数量的编译器指令,如:#define DEBUG, #undefine DEBUG, #if等。这些指令专属于C#,而且在数量上是固定的。而Attribute用作编译器指令则不受数量限制。详细看
程序集,模块,类型(类,结构,枚举,接口,委托),类成员(字段,方法、含构造,事件,属性(property)),参数(Parameter 、 方法返回值 、泛型参数),
Attribute使用场景
例如:Filter
,Validate(
, 密码|用户名
等数据验证 )MVC
/API
相关特性, AOP
应用等等
○ 注释是对程序源代码的一种说明,主要目的是给人看的,在程序被编译的时候会被编译器所丢弃,因此,它丝毫不会影响到程序的执行。
○ 而Attribute是程序代码的一部分,不但不会被编译器丢弃,而且还会被编译器编译进程序集(Assembly)的元数据(Metadata)里,在程序运行的时候,你随时可以从元数据里提取出这些附加信息来决策程序的运行。
举例:
在项目中,有一个类由两个程序员(小张和小李)共同维护。这个类起一个“工具包”(Utilities)的作用(就像.NET Framework中的Math类一样),里面含了几十个静态方法。而这些静态方法,一半是小张写的、一半是小李写的;在项目的测试中,有一些静态方法曾经出过bug,后来又被修正。这样,我们就可以把这些方面划分成这样几类:
我们分类的目的主要是在测试的时候可以按不同的类别进行测试、获取不同的效果。比如:统计两个人的工作量或者对曾经出过bug的方法进行回归测试。
如果不使用Attribute,为了区分这四类静态方法,我们只能通过注释来说明,但这种方式会有很多弊端;
如果使用Attribute,区分这四类静态方法将会变得简单多了。示例代码如下:
#define Buged
//C# 的宏定义必须出现在所有代码之前。当前只让 Buged 宏有效。
using System;
using System.Diagnostics; // 注意:这是为了使用包含在此名称空间中的ConditionalAttribute特性
namespace Con_Attribute
{
class Program
{
static void Main(string[] args)
{
// 虽然方法都被调用了,但只有符合条件的才会被执行!
ToolKit.FunA();
ToolKit.FunB();
ToolKit.FunC();
ToolKit.FunD();
}
}
class ToolKit
{
[ConditionalAttribute("Li")] // Attribute名称的长记法
[ConditionalAttribute("Buged")]
public static void FunA()
{
Console.WriteLine("Created By Li, Buged.");
}
[Conditional("Li")] // Attribute名称的短记法,这个就是实例化一个属性,这个实例附着于方法上
[Conditional("NoBug")]
public static void FunB()
{
Console.WriteLine("Created By Li, NoBug.");
}
[ConditionalAttribute("Zhang")]// Attribute名称的长记法,这个就是实例化一个属性,这个实例附着于方法上
[ConditionalAttribute("Buged")]
public static void FunC()
{
Console.WriteLine("Created By Zhang, Buged.");
}
[Conditional("Zhang")] // Attribute名称的短记法
[Conditional("NoBug")]
public static void FunD()
{
Console.WriteLine("Created By Zhang, NoBug.");
}
}
}
运行结果如下:
注意:运行结果是由代码中“#define Buged ”这个宏定义所决定。
分析:
1. 在本例中,我们使用了ConditionalAttribute 这个Attribute,它被包含在 System.Diagnostics 名称空间中。显然,它多半时间是用来做程序调试与诊断的。
2. 与ConditionalAttribute 相关的是一组C# 宏,它们看起来与C语言的宏别无二致,位置必须出现在所有C# 代码之前。顾名思义,ConditionalAttribute 是用来判断条件的,凡被ConditionalAttribute (或Conditional)“附着”了的方法,只有满足了条件才会执行。
3. Attribute 就像船底上可以附着很多牡蛎一样,一个方法上也可以附着多个ConditionalAttribute 的实例。把Attribute 附着在目标上的书写格式很简单,使用方括号把Attribute 括起来,然后紧接着写Attribute 的附着体就行了。当多个Attribute 附着在同一个目标上时,就把这些Attribute 的方括号一个挨一个地书写(或者在一对方括号中书写多个Attribute),而且不必在乎它们的顺序。
4. 在使用Attribute 的时候,有“长记法”和“短记法”两种,请君自便。
由上面的第3 条和第4 条我们可以推出,以下四种Attribute 的使用方式是完全等价:
// 长记法
[ConditionalAttribute("LI")]
[ConditionalAttribute("NoBug")]
public static void Fun()
{ Console.WriteLine("Created By Li, NoBug."); }
// 短记法
[Conditional("LI")]
[Conditional("NoBug")]
public static void Fun()
{ Console.WriteLine("Created By Li, NoBug."); }
// 换序
[Conditional("NoBug")]
[Conditional("LI")]
public static void Fun()
{ Console.WriteLine("Created By Li, NoBug."); }
// 单括号叠加
[Conditional("NoBug"), Conditional("LI")]
public static void Fun()
{ Console.WriteLine("Created By Li, NoBug."); }
以上等效于:
Conditional s=new Conditional("NoBug");
Attribute 的本质
从上面的代码中,我们可以看到Attribute 似乎总跟public、static 这些关键字(Keyword)出现在一起。
莫非使用了Attribute 就相当于定义了新的修饰符(Modifier)吗?让我们来一窥究竟!
示例代码如下:
#define XG //C# 的宏定义必须出现在所有代码之前
using System;
using System.Diagnostics; // 注意:这是为了使用包含在此名称空间中的ConditionalAttribute 特性
namespace Con_Attribute
{
class Program2
{
[Conditional("XG")]
static void Fun()
{
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine("http://xugang.cnblogs.com");
}
static void Main(string[] args)
{
Fun();
}
}
}
使用微软的中间语言反编译器查看 MSIL 中间语言中TargetMethod:void() 方法的代码,截图如下:
我们可以看到,在程序编译后,根据我们声明的特性标记,编译器自动为我们在对应位置注入了特性相关的代码,实现实例化。这就给了我们进一步的操作空间,我们可以利用特性追加功能,添加更丰富的信息。
仔细观察中间语言(MSIL)的代码之后,那些被C# 语言所掩盖的事实,在中间语言(MSIL)中就变得赤身裸体了。而Attribute 也变得毫无秘密!
图中红色所指的是Fun 方法及其修饰符,但Attribute 并没有出现在这里。
图中蓝色所指的是在调用mscorlib.dll 程序集中System.Diagnostics 名称空间中ConditionalAttribute 类的构造函数。
可见,Attribute 并不是修饰符,而是一个有着独特实例化形式的类!
1. 它的实例是使用.custom 声明的。查看中间语言语法,你会发现.custom 是专门用来声明自定义特性的。
2. 声明Attribute 的位置是在函数体内的真正代码(IL_0000 至IL_0014 )之前。
这就从“底层”证明了Attribute不是什么“修饰符”,而是一种实例化方式比较特殊的类。
MSIL 中间语言中,程序集的元数据(Metadata)记录了这个程序集里有多少个namespace、多少个类、类里有什么成员、成员的访问级别是什么。而且,元数据是以文本(也就是Unicode 字符)形式存在的,使用.NET的反射(Reflection)技术就能把它们读取出来,并形成MSIL 中的树状图、VS 里的Object Browser 视图,以及自动代码提示功能,这些都是元数据与反射技术结合的产物。一个程序集(.EXE或.DLL)能够使用包含在自己体内的元数据来完整地说明自己,而不必像C/C++ 那样带着一大捆头文件,这就叫作“自包含性”或“自描述性”。
Attribute 的实例化
就像牡蛎天生就要吸附在礁石或船底上一样,Attribute 的实例一构造出来就必需“粘”在一个什么目标上。
Attribute 实例化的语法是相当怪异的,主要体现在以下三点:
1. 不使用new 操作符来产生实例,而是使用在方括号里调用构造函数来产生实例。
2. 方括号必需紧挨着放置在被附着目标的前面。
3. 因为方括号里空间有限,不能像使用new 那样先构造对象,然后再给对象的属性(Property)赋值。
因此,对Attribute 实例的属性赋值也在构造函数的圆括号里。
并且,Attribute 实例化时尤其要注意的是:
1. 构造函数的参数是一定要写。有几个就得写几个,因为你不写的话实例就无法构造出来。
2. 构造函数参数的顺序不能错。调用任何函数都不能改变参数的顺序,除非它有相应的重载(Overload)。因为这个顺序是固定的,有些书里称其为“定位参数”(意即“个数和位置固定的参数”)。
3. 对Attribute 实例的属性的赋值可有可无。反正它会有一个默认值,并且属性赋值的顺序不受限制。有些书里称属性赋值的参数为“具名参数”。
定义特性和自定义特性。
对于一个自定义的Attribute,你可以通过AttributeUsage的Attribute来限定你的Attribute 所施加的元素的类型。代码形式如下:
[AttriubteUsage(参数设置)] public 自定义Attribute : Attribute { ... }
1.系统预定义特性
在.net框架内提供了三种预定义特性,经常使用特性或对特性有了解的朋友肯定见到过或用过。
- AttributeUsage
- Conditional
- obsolete
【AttributeUsage】
用例:[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false )]
预定义特性 AttributeUsage 描述了如何使用一个自定义特性类。它规定了特性可应用到的项目的类型。使用AttributeUsage 特性有个前提,该类必须继承Attribute抽象类。
下面是AttributeTargets 的定义:
[Flags]
public enum AttributeTargets
{
All=16383,
Assembly=1,
Module=2,
Class=4,
Struct=8,
Enum=16,
Constructor=32,
Method=64,
Property=128,
Field=256,
Event=512,
Interface=1024,
Parameter=2048,
Delegate=4096,
ReturnValue=8192
}
作为参数的AttributeTarges的值允许通过“|”操作来进行多个值得组合,如果你没有指定参数,那么默认参数就是All 。 AttributeUsage除了继承Attribute 的方法和属性之外,还定义了以下三个属性:
AllowMultiple: 读取或者设置这个属性,表示是否可以对一个程序元素施加多个Attribute 。
Inherited:读取或者设置这个属性,表示是否施加的Attribute 可以被派生类继承或者重载。
下面让我们来做一些实际的东西。我们将会在Help特性前放置AttributeUsage特性以期待在它的帮助下控制Help特性的使用。
using System; [AttributeUsage(AttributeTargets.Class, AllowMultiple = false,Inherited = false )] public class HelpAttribute : Attribute { public HelpAttribute(String Description_in) { this.description = Description_in; } protected String description; public String Description { get { return this.description; } } }
先让我们来看一下AttributeTargets.Class。它规定了Help特性只能被放在class的前面。这也就意味着下面的代码将会产生错误:
[Help("this is a do-nothing class")] public class AnyClass { [Help("this is a do-nothing method")] //error public void AnyMethod() { } }
编译器报告错误如下:
AnyClass.cs: Attribute 'Help' is not valid on this declaration type.
It is valid on 'class' declarations only.
我们可以使用AttributeTargets.All来允许Help特性被放置在任何程序实体前。可能的值是:
Assembly,Module,Class,Struct,Enum,Constructor,Method,Property,Field,Event,Interface,
Parameter,Delegate。
All = Assembly | Module | Class | Struct | Enum | Constructor | Method | Property | Field | Event | Interface | Parameter | Delegate,
ClassMembers = Class | Struct | Enum | Constructor | Method | Property | Field | Event | Delegate | Interface )
下面考虑一下AllowMultiple = false。它规定了特性不能被重复放置多次。
[Help("this is a do-nothing class")] [Help("it contains a do-nothing method")] public class AnyClass { [Help("this is a do-nothing method")] //error public void AnyMethod() { } }
它产生了一个编译期错误。
AnyClass.cs: Duplicate 'Help' attribute
Ok,现在我们来讨论一下最后的这个属性。Inherited, 表明当特性被放置在一个基类上时,它能否被派生类所继承。
[Help("BaseClass")] public class Base { } public class Derive : Base { }
这里会有四种可能的组合:
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false) ]
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false) ]
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true )]
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true )]
第一种情况:
如果我们查询(Query)(稍后我们会看到如何在运行期查询一个类的特性)Derive类,我们将会发现Help特性并不存在,因为inherited属性被设置为false。
第二种情况:
和第一种情况相同,因为inherited也被设置为false。
第三种情况:
为了解释第三种和第四种情况,我们先来给派生类添加点代码:
[Help("BaseClass")] public class Base { } [Help("DeriveClass")] public class Derive : Base { }
现在我们来查询一下Help特性,我们只能得到派生类的属性,因为inherited被设置为true,但是AllowMultiple却被设置为false。因此基类的Help特性被派生类Help特性覆盖了。
第四种情况:
在这里,我们将会发现派生类既有基类的Help特性,也有自己的Help特性,因为AllowMultiple被设置为true。
【Conditional】和【Obsolete】
Conditional:起条件编译的作用,只有满足条件,才允许编译器对它的代码进行编译。一般在程序调试的时候使用。
DllImport:用来标记非.NET的函数,表明该方法在一个外部的DLL中定义。
Obsolete:这个属性用来标记当前的方法已经被废弃,不再使用了。
下面的代码演示了上述三个属性的使用:
#define DEBUG //C# 的宏定义必须出现在所有代码之前
using System; using System.Runtime.InteropServices;
using System.Diagnostics;
namespace AttributeDemo {
class MainProgramClass
{
[DllImport("User.dll")]
public static extern int MessageBox(int hParent, string Message, string Caption, int Type);
static void Main(string[] args) { DisplayRunningMessage(); DisplayDebugMessage();
MessageBox(0,"Hello","Message",0); Console.ReadLine(); } [Conditional("DEBUG")]
private static void DisplayRunningMessage()
{
Console.WriteLine("开始运行Main子程序。当前时间是"+DateTime.Now);
} [Conditional("DEBUG")]
//[Obsolete("Don't use Old method, use New method", true)]
[Obsolete]
private static void DisplayDebugMessage() {
Console.WriteLine("开始Main子程序");
}
}
}
如果在一个程序元素前面声明一个Attribute,那么就表示这个Attribute被施加到该元素上,前面的代码,[DllImport]施加到MessageBox函数上, [Conditional]施加到DisplayRuntimeMessage方法和DisplayDebugMessage方法,[Obsolete]施加到DisplayDebugMessage方法上。
根据上面涉及到的三个Attribute的说明,我们可以猜到程序运行的时候产生的输出:DllImport Attribute表明了MessageBox是User32.DLL中的函数,这样我们就可以像内部方法一样调用这个函数。
重要的一点就是Attribute就是一个类,所以DllImport也是一个类,Attribute类是在编译的时候被实例化的,而不是像通常的类那样在运行时候才实例化。Attribute实例化的时候根据该Attribute类的设计可以带参数,也可以不带参数,比如DllImport就带有"User32.dll"的参数。Conditional对满足参数的定义条件的代码进行编译,如果没有定义DEBUG,那么该方法将不被编译,读者可以把#define DEBUG一行注释掉看看输出的结果(release版本,在Debug版本中Conditional的debug总是成立的)。Obsolete表明了DispalyDebugMessage方法已经过时了,它有一个更好的方法来代替它,当我们的程序调用一个声明了Obsolete的方法时,那么编译器会给出信息,Obsolete还有其他两个重载的版本。大家可以参考msdn中关于的ObsoleteAttribute 类的描述。
2、自定义Attribute 实例
在此,我们不使用.NET Framework 中的各种Attribute 系统特性,而是从头自定义一个全新的Attribute 类。
示例代码如下:
using System;
namespace Con_Attribute
{
class Program3
{
static void Main(string[] args)
{
//使用反射读取Attribute
System.Reflection.MemberInfo info = typeof(Student); //通过反射得到Student类的信息
Hobby hobbyAttr = (Hobby)Attribute.GetCustomAttribute(info, typeof(Hobby));
if (hobbyAttr != null)
{
Console.WriteLine("类名:{0}", info.Name);
Console.WriteLine("兴趣类型:{0}", hobbyAttr.Type);
Console.WriteLine("兴趣指数:{0}", hobbyAttr.Level);
}
}
}
//注意:"Sports" 是给构造函数的赋值, Level = 5 是给属性的赋值。
[Hobby("Sports", Level = 5)]
class Student
{
[Hobby("Football")]
public string profession;
public string Profession
{
get { return profession; }
set { profession = value; }
}
}
//建议取名:HobbyAttribute
class Hobby : Attribute // 必须以System.Attribute 类为基类
{
// 参数值为null的string 危险,所以必需在构造函数中赋值
public Hobby(string _type) // 定位参数
{
this.type = _type;
}
//兴趣类型
private string type;
public string Type
{
get { return type; }
set { type = value; }
}
//兴趣指数
private int level;
public int Level
{
get { return level; }
set { level = value; }
}
}
}
为了不让代码太长,上面的示例中Hobby 类的构造函数只有一个参数,所以对“定位参数”体现的还不够淋漓尽致。大家可以为Hobby 类再添加几个属性,并在构造函数里多设置几个参数,体验一下Attribute 实例化时对参数个数及参数位置的敏感性。
能被Attribute 所附着的目标
Attribute 可以将自己的实例附着在什么目标上呢?这个问题的答案隐藏在AttributeTargets 这个枚举类型里。
这个类型的可取值集合为:
=========================================================================================================
All Assembly Class Constructor
Delegate Enum Event Field
GenericParameter Interface Method Module
Parameter Property ReturnValue Struct
=========================================================================================================
一共是16 个可取值。上面这张表是按字母顺序排列的,并不代表它们真实值的排列顺序。
使用下面这个小程序可以查看每个枚举值对应的整数值,示例代码如下:
using System;
namespace Con_Attribute
{
class Program4
{
static void Main(string[] args)
{
Console.WriteLine("Assembly\t\t\t{0}", Convert.ToInt32(AttributeTargets.Assembly));
Console.WriteLine("Module\t\t\t\t{0}", Convert.ToInt32(AttributeTargets.Module));
Console.WriteLine("Class\t\t\t\t{0}", Convert.ToInt32(AttributeTargets.Class));
Console.WriteLine("Struct\t\t\t\t{0}", Convert.ToInt32(AttributeTargets.Struct));
Console.WriteLine("Enum\t\t\t\t{0}", Convert.ToInt32(AttributeTargets.Enum));
Console.WriteLine("Constructor\t\t\t{0}", Convert.ToInt32(AttributeTargets.Constructor));
Console.WriteLine("Method\t\t\t\t{0}", Convert.ToInt32(AttributeTargets.Method));
Console.WriteLine("Property\t\t\t{0}", Convert.ToInt32(AttributeTargets.Property));
Console.WriteLine("Field\t\t\t\t{0}", Convert.ToInt32(AttributeTargets.Field));
Console.WriteLine("Event\t\t\t\t{0}", Convert.ToInt32(AttributeTargets.Event));
Console.WriteLine("Interface\t\t\t{0}", Convert.ToInt32(AttributeTargets.Interface));
Console.WriteLine("Parameter\t\t\t{0}", Convert.ToInt32(AttributeTargets.Parameter));
Console.WriteLine("Delegate\t\t\t{0}", Convert.ToInt32(AttributeTargets.Delegate));
Console.WriteLine("ReturnValue\t\t\t{0}", Convert.ToInt32(AttributeTargets.ReturnValue));
Console.WriteLine("GenericParameter\t\t{0}", Convert.ToInt32(AttributeTargets.GenericParameter));
Console.WriteLine("All\t\t\t\t{0}", Convert.ToInt32(AttributeTargets.All));
Console.WriteLine("\n");
}
}
}
结果显示如下:
AttributeTargets 使用了枚举值的另一种用法 —— 标识位。
除了All 的值之外,每个值的二进制形式中只有一位是“1”,其余位全是“0”。
如果我们的Attribute 要求既能附着在类上,又能附着在类的方法上。就可以使用C# 中的操作符“|”(也就是按位求“或”)。有了它,我们只需要将代码书写如下:
AttributeTargets.Class | AttributeTargets.Method
因为这两个枚举值的标识位(也就是那个唯一的“1”)是错开的,所以只需要按位求或就解决问题了。
这样,你就能理解:为什么AttributeTargets.All 的值是32767 了。
默认情况下,当我们声明并定义一个新的Attribute 类时,它的可附着目标是AttributeTargets.All。
大多数情况下,AttributeTargets.All 就已经满足需求了。不过,如果你非要对它有所限制,那就要费点儿周折了。
例如,你想把前面的Hobby 类的附着目标限制为只有“类”和“字段”使用,则示例代码如下:
[AttributeUsage(AttributeTargets.Class, AttributeTargets.Field)]
class Hobby : Attribute // 必须以System.Attribute 类为基类
{
// Hobby 类的具体实现
}
这里是使用Attribute的实例(AttributeUsage)附着在Attribute 类(Hobby)上。Attribute 的本质就是类,而AttributeUsage 又说明Hobby 类可以附着在哪些类型上。
附加问题:
1. 如果一个Attribute 类附着在了某个类上,那么这个Attribute 类会不会随着继承关系也附着在派生类上呢?
2. 可不可以像多个牡蛎附着在同一艘船上那样,让一个Attribute 类的多个实例附着在同一个目标上呢?
答案:可以。代码如下:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Field, Inherited = false, AllowMultiple = true)]
class Hobby : System.Attribute
{
// Hobby 类的具体实现
}
AttributeUsage 这个专门用来修饰Attribute 的Attribute ,除了可以控制修饰目标外,还能决定被它修饰的Attribute 是否可以随宿主“遗传”,以及是否可以使用多个实例来修饰同一个目标!
那修饰ConditionalAttribute 的AttributeUsage 又会是什么样子呢?(答案在MSDN中)