定制特性仅仅是为目标元素提供关联附加信息的一种方式。编译器的工作只是将这些附加信息存放在托管模块的元数据中而已。大多数特性对编译器没有任何意义。编译器仅仅只是检测源代码中的定制特性,然后产生相应的元数据。
CLR允许将特性应用于任何可在一个文件的元数据中表示的元素(如:TypeDef、MethodDef、ParamDef、FieldDef、PropertyDef、EventDef、AssemblyDef、ModuleDef),也可以应该到元数据引用表中的条目上(如:AssemblyRef、ModuleRef、TypeRef、MemberRef),还可以应用到其他一些元数据中(如:安全许可、导出类型及资源等)。
但C#只允许我们将特性应用于各种元数据定义表中的条目上。
具体而言,C#只允许我们将特性应用于定义了以下构造的源代码中:程序集、模块、类型、字段、方法、方法参数、方法返回值、属性、事件。
当我们应用一个特性时,C#允许我们指定一个前缀来表示特性所应用的目标元素。在许多情况下,如果我们省去前缀,编译器仍会判断出特性所应用的目标元素。
示例代码:
[assembly:MyAttribute(1)] //应用于程序集上
[module:MyAttribute(2)] //应用于模块上
[type:MyAttribute(3)] //应用于类型上
class SomeType
{
[properety:MyAttribute(4)] //应用于属性上
public String SomeProp {get {return null;}}
[event:MyAttribute(5)] //应用于事件上
public event EventHandler SomeEvent;
[field:MyAttribute(6)] //应用于字段上
public int SomeField = 0;
[return:MyAttribute(7)] //应用于返回值上
[method:MyAttribute(8)] //应用于方法上
public int SomeMethod(
[param:MyAttribute(9)] //应用于参数上
int SomeParam)
{ return SomeParam; }
}
实际上,一个特性仅仅是一个类型的实例。要与通用语言规范(CLS)兼容,定制特性的类型必须直接或间接继承自System.Attribute。C#只允许使用与CLS兼容的定制特性。
如前所述,一个特性就是类型的一个实例,该类型必须有一个公有构造器来创建它的实例。所有当我们在一个目标元素上应用一个特性时,其语法类似于调用类型的实例构造器。另外,一些语言可能允许我们使用一些特殊的语法来设置特性类型的公有字段或属性。
示例代码:
[DllImport("Kernel32", CharSet=CharSet.Auto, SetLastError=true)]
我们查看DllImportAttribute类型文档,它的构造器仅要求一个String参数。上例中,Kernel32就是作为String参数被传入。一个特性类型的构造器参数被称为定位参数,该参数是强制性的,即在应用特性时必须指定这样的参数。
那另两个参数呢?上面的特殊语法允许我们在DllImportAttribute对象构造之后设置它的公有字段或属性。上例中,当DllImportAttribute对象被构造时,实际发生了3件事:"Kernel32"被传入DllImportAttribute构造器,CharSet被设为CharSet.Auto,SetLastError被设为true。设置字段或属性的“参数”被称为命名参数,它们是可选的。
我们可以在一个目标元素上应用多个不同的特性。当在一个目标元素上应用多个特性时,它们的顺序可以任意。