24.2 特性(Attribute)

这里讲的Attribute,翻译成中文也是“属性”的意思,但为了不与在第10.4.5节讲的“属性”相混淆,我们这里使用“特性”的名称以示区别。做个简单对比,例如,代码清单24-4中的Name和Grade就是属性,它的作用是对字段的封装。

代码清单24-4 属性的例子


public class Student

{

//属性

public string Name

{

get;

set;

}

//属性

public string Grade

{

get;

set;

}

}


而特性的作用是:为类型、方法、属性添加附加的声明信息。在运行时可以使用反射技术获取这些声明信息,这在很多场合非常有用,如文档生成、O/R Mapping(对象关系映射,稍后会举一个O/R Mapping场景中应用的简单例子)。并非只有C#中有特性的概念,在Java中的对应技术为Annotation(JDK1.5时新引入),中文名为注解。事实上,在.NET中已经提供了一些预定义特性,说不定在不经意中已经使用到了其中的一些。不信可以打开工程中名为AssemblyInfo.cs的文件,它位于工程目录下的Properties目录中,如图24-11所示。

24.2 特性(Attribute) - 图1

图 24-11 AssemblyInfo.cs的位置

文件的内容如代码清单24-5所示,这是VisualStudio自动生成的文件,用于定义一些全局信息。

代码清单24-5 AssemblyInfo.cs文件的内容


using System.Reflection;

using System.Runtime.CompilerServices;

using System.Runtime.InteropServices;

//有关程序集的常规信息通过以下

//特性集控制。更改这些特性值可修改

//与程序集关联的信息

[assembly:AssemblyTitle("ProgrammingCSharp4")]

[assembly:AssemblyDescription(“")]

[assembly:AssemblyConfiguration(“")]

[assembly:AssemblyCompany(“")]

[assembly:AssemblyProduct("ProgrammingCSharp4")]

[assembly:AssemblyCopyright("Copyright©2010")]

[assembly:AssemblyTrademark(“")]

[assembly:AssemblyCulture(“")]

//将ComVisible设置为false使此程序集中的类型

//对COM组件不可见。如果需要从COM访问此程序集中的类型,

//则将该类型上的ComVisible特性设置为true。

[assembly:ComVisible(false)]

//如果此项目向COM公开,则下列GUID用于类型库的ID

[assembly:Guid("08bb2c82-5c23-4bdc-a72e-db53395618f5")]

//程序集的版本信息由下面四个值组成:

//

//主版本

//次版本

//内部版本号

//修订号

//

//可以指定所有这些值,也可以使用“内部版本号”和“修订号”的默认值,

//方法是按如下所示使用"*":

[assembly:AssemblyVersion("1.0.*")]

[assembly:AssemblyVersion("1.0.0.0")]

[assembly:AssemblyFileVersion("1.0.0.0")]


此外,标识一个类可以序列化的Serializable特性,在第18章讲深复制的时候使用过。另外,标识某些成员不参与序列化的特性为NonSerialized等。回想一下,当时我们是如何去使用这些特性的,下面是一个例子,如代码清单24-6所示。

代码清单24-6 特性的例子


[Serializable]

public class Person

{

[NonSerialized]

public string birthday;

public Person(ArrayList favourites)

{

this.favourites=favourites;

}

}


可见,特性是包括在一对方括号中的,它修饰的对象可以是程序集(Assembly)、类、方法、属性等。换句话说,它可以为程序集、类、方法、属性等提供额外的声明信息。

24.2.1 什么是特性

如果你使用过C++,或许对包含关键字(如public和private)的声明比较熟悉,这些关键字提供有关类成员的其他信息。另外,这些关键字通过描述类成员对其他类的可访问性来进一步定义类成员的行为。由于编译器被显式设计为识别预定义关键字,因此传统上您没有机会创建自己的关键字。但是,公共语言运行时允许您添加类似关键字的描述性声明(称为特性)来批注编程元素,如类型、字段、方法和属性。

为运行时编译代码时,该代码被转换为CIL(Common Intermediate Language,公共中间语言),并同编译器生成的元数据一起被放到可迁移可执行(Portable Executable,PE)文件的内部。特性使得我们可以向元数据中放置额外的描述性信息,并可使用运行时反射服务提取该信息。任何特性都是System.Attribute抽象类的直接或间接的派生类。因此,要创建一个自定义的特性,就需要从System.Attribute抽象类继承,或者派生自另一个特性。

.NET Framework出于多种原因使用特性并通过它们解决若干问题。特性描述如何将数据序列化,指定用于强制安全性的特性,并限制实时(Just-In-Time,JIT)编译器的优化,从而使代码易于调试。特性还可以记录文件名或代码作者,或在窗体开发阶段控制控件和成员的可见性。

可使用特性以几乎所有可能的方式描述代码,并以富有创造性的新方式影响运行时行为。使用特性可以向C#、Visual C++、Microsoft Visual Basic 2005或其他任何以运行时为目标的语言添加自己的描述性元素,而不必重新编写编译器。

按照约定,所有特性名都以Attribute结尾。但是,对于C#等以运行时为目标的语言,不要求指定特性的全名。例如,如果要应用HelpAttribute、DocumentAttribute特性,只需使用Help、Document作为特性名称即可。

特性本身也需要说明特性的元特性,可能不容易理解,这么说吧,特性本身可以应用到哪些对象(程序集、类等),特性是否允许在同一个对象多次使用,以及特性是否会在被修饰对象的派生类同样起作用等,这些都是通过元特性进行说明的,最常用的元特性是AttributeUsage。

1.定位参数和命名参数

特性可以与方法和属性相同的方式接受参数,它有两种类型的参数:定位参数和命名参数。使用参数可以向特性类的字段或属性赋值,由此改变一个特性实例的状态。但特性并非必须使用参数,它也可以没有任何参数。但是,如果有参数的话,则只能是这两种类型参数中的一种。

以代码清单24-7中的HelpAttribute特性为例,分别举例阐述定位参数和命名参数。

代码清单24-7 特性示例


using System;

[AttributeUsage(AttributeTargets.Class)]

public class HelpAttribute:Attribute

{

public HelpAttribute(string url){……

}

public string Topic{

get{……}

set{……}

}

public string Url{get{……}}

}


(1)定位参数:特性类中的每个公共非静态构造函数定义了一系列有序的定位参数。顺序很重要,定位参数和命名参数之间也是有顺序的,定位参数在前,不需要参数名,直接在参数的对应位置传入参数值即可。如代码清单24-8所示,只有一个定位参数,没有使用命名参数。这里的定位参数对应于特性类HelpAttribute的公共构造函数:HelpAttribute(string url)。

代码清单24-8 定位参数


[Help("http://www.mycompany.com/……/Class1.htm")]

class Class1

{

}


定位参数由特性类的公共构造函数决定,特性类可以有多个公共构造函数,HelpAttribute类增加了一个构造函数:


public HelpAttribute(string url,bool isOpenInNewWindow){

……

}


如代码清单24-9所示。

代码清单24-9 有多个构造函数的特性类


using System;

[AttributeUsage(AttributeTargets.Class)]

public class HelpAttribute:Attribute

{

public HelpAttribute(string url){

……

}

public HelpAttribute(string url,bool isOpenInNewWindow){

……

}

public string Topic{

get{……}

set{……}

}

public string Url{get{……}}

}


此时HelpAttribute有两个构造函数:

❑HelpAttribute(string url)

❑HelpAttribute(string url,bool isOpenInNewWindow)

那么,Help特性的定位参数也对应着两种形式,如代码清单24-10所示。

代码清单24-10 Help特性的两种形式


[Help("http://www.mycompany.com/……/Class1.htm")]

class Class1

{

}

[Help("http://www.mycompany.com/……/Class1.htm",true)]

class Class2

{

}


(2)命名参数:特性类中的每一个非静态的公共可读写字段或属性都是一个命名参数,命名参数的形式为:“参数名=参数值”。注意这里的关键词——可读写,只读属性不能作为命名参数。可选择性地对命名参数赋值,如果有多个命名参数,它们之间的顺序可以随意调整。如代码清单24-11中的Topic就是一个命名参数,它对应着HelpAttribute类中的Topic属性。

代码清单24-11 命名参数


[Help("http://www.mycompany.com/……/Misc.htm",Topic="Class2")]

class Class2

{

}


也可以不使用命名参数:Topic,那么代码就和代码清单24-8一样了,因为篇幅原因故省略。

图24-12演示了定位参数和命名参数。

24.2 特性(Attribute) - 图2

图 24-12 定位参数和命名参数

2.特性的参数类型

无论是定位参数还是命名参数,其参数类型都是有限制的,只能为以下类型:

❑bool、byte、char、double、float、int、long、short、string;

❑object类型和System.Type类型;

❑公共枚举类型,且枚举类型中内嵌的类型也是公共的;

❑上述类型组成的一维数组。

3.应用特性

要将一个特性应用到程序元素非常简单,只需以下步骤即可:

(1)在紧邻程序元素之前放置特性,从而将该特性应用于程序元素。在C#中,特性由方括号括起来,并且通过空白(可包括换行符)与元素分隔。

(2)为特性指定位置参数和命名参数。位置参数是必需的,并且必须放在所有命名参数之前;位置参数对应于特性的公共构造函数之中的参数。而命名参数是可选的,对应于特性的公共可读/写属性。在C#中,为每个可选参数指定name=value,其中name是属性的名称。

上述如图24-13所示。

24.2 特性(Attribute) - 图3

图 24-13 特性的组成和使用

一般情况下,特性应用于它后面的程序元素。但是,你也可以显式地指定要将特性应用于方法还是参数抑或返回值。若要显式指明特性应用的程序元素,可以使用下面的语法:


[target:attribute-list]


其中target可能的值,如表24-2所示。

24.2 特性(Attribute) - 图4

其中,方括号中是以逗号分隔的一个或多个特性的列表,如代码清单24-12所示。

代码清单24-12 使用逗号分隔的多个特性


using System.Diagnostics;

public class App

{

Sample1("DEBUG"),Sample2("TEST1"),[[1]]

public void Process()

{

}

}


要说明的是,特性列表中的各个特性顺序并不重要,代码清单24-13和代码清单24-12完全等价。

代码清单24-13 调整了顺序的特性列表


[Sample2("TEST1"),Sample1("DEBUG")]

public void Process(){}

[Sample1("DEBUG")]

[Sample2("TEST1")]

public void Process(){}

[Sample2("TEST1")]

[Sample1("DEBUG")]

public void Process(){}


[1]此处允许使用逗号结尾。