自定义特性
你或许已经注意到了,应用特性的语法和之前见过的其他语法有很大不同。你可能会觉得特
性是一种完全不同的结构类型,其实不是,特性只是一种特殊的类。
有关特性类的一些要点如下。
- 用户自定义的特性类叫作自定义特性。
- 所有特性类都派生自System.Attribute。
声明自定义特性
总体来说,声明一个特性类和声明其他类一样。然而,有一些事项值得注意,如下所示。
- 要声明一个自定义特性,需要做如下工作。
- 声明一个派生自System.Attribute的类。
- 给它起一个以后缀Attribute结尾的名字。
- 安全起见,通常建议你声明一个sealed的特性类。
例如,下面的代码显示了MyAttributeAttribute特性的声明的开始部分:
由于特性持有目标的信息,所有特性类的公有成员只能是:
- 字段
- 属性
- 构造函数
使用特性的构造函数
特性和其他类一样,有构造函数。每一个特性必须至少有一个公共构造函数。
- 和其他类一样,如果你不声明构造函数,编译器会为你产生一个隐式、公共且无参的构
造函数。 - 特性的构造函数和其他构造函数一样,可以被重载。
- 声明构造函数时必须使用类全名,包括后缀。只可以在应用特性时使用短名称。
例如,如果有如下的构造函数(名字没有包含后缀),编译器会产生一个错误消息
public MyAttributeAttribute(string desc,string ver)
{Description = desc;VersionNumber = ver;
}
指定构造函数
当我们为目标应用特性时,其实是在指定应该使用哪个构造函数来创建特性的实例。列在特
性应用中的参数其实就是构造函数的实参。
例如,在下面的代码中,MyAttribute被应用到一个字段和一个方法上。对于字段,声明指定
了使用带单个字符串参数的构造函数。对于方法,声明指定了使用带两个字符串参数的构造函数。
[MyAttribute("Holds a value")] //使用一个字符串的构造函数
public int MyField;[MyAttribute("Version 1.3", "Galen Daniel")] //使用两个字符串的构造函数
public void MyMethod()
{...
}
特性构造函数的其他要点如下。
- 在应用特性时,构造函数的实参必须是在编译时能确定值的常量表达式。
- 如果应用的特性构造函数没有参数,可以省略圆括号。例如,如下代码中的两个类都使
用MyAttr特性的无参构造函数。两种形式的意义是相同的。
[MyAttr]
class SomeClass...[MyAttr]
class OtherClass...
使用构造函数
和其他类一样,我们不能显式调用构造函数。特性的实例被创建后,只有特性的消费者访问
特性时才能调用构造函数。这一点与其他类的实例不同,这些实例都创建在使用对象创建表达式
的位置。应用一个特性是一条声明语句,它不会决定什么时候构造特性类的对象。
图25-4比较了普通类构造函数的使用和特性的构造函数的使用。
- 命令语句的实际意义是:“在这里创建新的类。
- 声明语句的意义是:“这个特性和这个目标相关联,如果需要构造特性,则使用这个构造
函数。
构造函数中的位置参数和命名参数
与普通类的方法和构造函数相似,特性的构造函数同样可以使用位置参数和命名参数。
如下代码显示了使用一个位置参数和两个命名参数来应用一个特性:
[MyAttribute("An execellent class",Reviewer="Amy McArthur",Ver="0.7.15.33)]
下面的代码演示了特性类的声明以及为MyClass类应用特性。注意,构造函数的声明只列出了
一个形参,但我们可通过命名参数给构造函数3个实参。两个命名参数设置了字段ver和Reviewer
的值。
public sealed class MyAttributeAttribute:System.Attribute
{public string Description;public string Ver;public string Reviewer;public MyAttributeAttribute(string desc) //一个形参{Description=desc;}
}[MyAttribute("An execellent class",Reviewer="Amy McArthur",Ver="7.15.33")]
class MyClass
{...
}
说明 和方法一样,构造函数需要的任何位置参数都必须放在命名参数之前。
限制特性的使用
我们已经看到了可以为类应用特性。但特性本身就是类,有一个很重要的预定义特性可以应
用到自定义特性上,那就是AttributeUsage特性。我们可以使用它来限制将特性用在某个目标类
例如,如果希望自定义特性MyAttribute只能应用到方法上,那么可以以如下形式使用
AttributeUsage:
[AttributeUsage(AttributeUsage.Method)]
public sealed class MyAttributeAttribute:System.Attribute
{...
}
AttributeUsage有3个重要的公有属性,如表25-4所示。表中显示了属性名和属性的含义。
对于后两个属性,还显示了它们的默认值。
名 字 | 定 义 | 默 认 值 |
---|---|---|
ValidOn | 保存能应用特性的目标类型列表。构造函数的第一个参数必须是 AttributeTargets 类型的枚举值 | |
Inherited | 一个布尔值,它指示特性是否可被装饰类型的派生类所继承 | true |
AllowMultiple | 一个布尔值,指示目标上是否可应用特性的多个实例 | false |
AttributeUsage的构造函数
AttributeUsage的构造函数接受单个位置参数,该参数指定了可使用特性的目标类型。它用这
个参数来设置Valid0n属性,可接受的目标类型是AttributeTargets枚举的成员。AttributeTargets
枚举的完整成员列表如表25-5所示。
可以通过使用按位或运算符来组合使用类型。例如,在下面的代码中,被装饰的特性只能应
用到方法和构造函数上。
[AttributeUsage(AttributeTarget.Method|AttributeTarget.Constructor)]
public sealed class MyAttributeAttribute:System.Attribute
{...
}
All | Assembly | Class | Constructor |
---|---|---|---|
Delegate | Enum | Event | Field |
GenericParameter | Interface | Method | Module |
Parameter | Property | ReturnValue | Struct |
当为特性声明应用AttributeUsage时,构造函数至少需要一个必需的参数,参数包含的目标
类型会保存在Valid0n中。还可以通过使用命名参数有选择地设置lnherited和AllowMu1tiple
属性。如果不设置,它们会保持如表25-4所示的默认值。
作为示例,下面一段代码指定了MyAttribute的如下方面。
- MyAttlibute能且只能应用到类上。
- MyAttribute不会被应用它的类的派生类所继承。
- 不能在同一个目标上应用MyAttribute的多个实例。
[AttributeUsage(AttributeTarget.Class,//必需的位置参数Inherited=false, //可选的命名参数AllowMultiple=false)] //可选的命名参数public sealed class MyAttributeAttribute:System.Attribute
{...
}
自定义特性的最佳实践
强烈推荐编写自定义特性时参考如下实践。
- 特性类应该表示目标结构的某种状态。
- 如果特性需要某些字段,可以通过包含具有位置参数的构造函数来收集数据,可选字段
可以采用命名参数按需初始化。 - 除了属性之外,不要实现公有方法或其他函数成员。
- 为了更安全,把特性类声明为sealed。
- 在特性声明中使用AttributeUsage来显式指定特性目标组。
如下代码演示了这些准则:
[AttributeUsage(AttributeTargets.Class)]
public sealed class ReviewCommentAttribute:System.AttributeTargets
{public string Description{get;set;}public string VersionNumber{get;set;}public string ReviewerID{get;set;}public ReviewCommentAttribute(string desc,string ver){Description=desc;VersionNumber=ver;}
}