程序员 C# 入门 - 第 21 章






4.92/5 (9投票s)
2000 年 11 月 22 日

81067

1601
Erik Gunnerson 撰写了关于 C# 中属性的文章。
![]() |
|
属性
在大多数编程语言中,一部分信息通过声明来表达,另一部分信息通过代码来表达。例如,在下面的类成员声明中:
public int Test;
编译器和运行时会为整型变量保留空间,并将其保护设置为随处可见。这是声明式信息的一个例子;它之所以好,是因为表达经济且编译器为我们处理细节。
通常,声明式信息的类型由语言设计者预定义,并且不能被语言的用户扩展。例如,想要将特定的数据库字段与类字段关联起来的用户,必须想出一种在语言中表达这种关系的方法,一种存储关系的方法,以及一种在运行时访问信息的方法。在 C++ 这样的语言中,可能会定义一个宏,该宏将信息存储在对象的一部分字段中。这样的方案有效,但容易出错且不通用。它们也很丑陋。
.NET 运行时支持特性(attributes),它们仅仅是放置在源代码元素(如类、成员、参数等)上的注释。特性可用于更改运行时的行为,提供关于对象的事务信息,或向设计人员传达组织信息。特性信息与元素的元数据一起存储,并且可以通过称为反射的过程在运行时轻松检索。
C# 使用条件特性来控制何时调用成员函数。条件特性的用法如下所示:
using System.Diagnostics;
class Test
{
[Conditional("DEBUG")]
public void Validate()
{
}
}
大多数程序员使用预定义特性的频率会远高于编写特性类。
使用特性
假设一个小组正在进行一个项目,需要跟踪已对类执行的代码审查,以便确定代码审查何时完成。代码审查信息可以存储在数据库中,这样可以方便地查询状态,也可以存储在注释中,这样可以方便地同时查看代码和信息。
或者可以使用特性,这样可以同时实现这两种访问方式。
要做到这一点,需要一个特性类。特性类定义了特性的名称、如何创建它以及将要存储的信息。定义特性类的细节将在“你自己定义一个特性”一节中介绍。
特性类看起来会是这样:
using System;
[AttributeUsage(AttributeTargets.Class)]
public class CodeReviewAttribute: System.Attribute
{
public CodeReviewAttribute(string reviewer, string date)
{
this.reviewer = reviewer;
this.date = date;
}
public string Comment
{
get
{
return(comment);
}
set
{
comment = value;
}
}
public string Date
{
get
{
return(date);
}
}
public string Reviewer
{
get
{
return(reviewer);
}
}
string reviewer;
string date;
string comment;
}
[CodeReview("Eric", "01-12-2000", Comment="Bitchin' Code")]
class Complex
{
}
类前面的 AttributeUsage
特性指定此特性只能放置在类上。当特性应用于程序元素时,编译器会检查该特性在该程序元素上的使用是否被允许。
特性的命名约定是在类名末尾附加 Attribute
。这使得更容易区分哪些类是特性类,哪些类是普通类。所有特性都必须继承自 System.Attribute
。
该类定义了一个接受审查者和日期作为参数的单个构造函数,并且还有一个公共字符串 Comment
。
当编译器遇到类 Complex
上的特性用法时,它首先查找一个从 Attribute
派生的名为 CodeReview
的类。它找不到,所以它接着查找一个名为 CodeReviewAttribute
的类,并找到了它。
接下来,它会检查该特性是否允许用于类。
然后,它会检查是否存在一个构造函数,其参数与我们在特性使用中指定的参数相匹配。如果找到一个,就会创建一个对象实例——该构造函数会使用指定的值被调用。
如果存在命名参数,它会匹配参数的名称与特性类中的字段或属性,然后将字段或属性设置为指定的值。
完成这些之后,特性类的当前状态就会被保存到为之指定的程序元素的元数据中。
至少,这是逻辑上发生的情况。实际上,它只是看起来是这样发生的;有关其实现方式的描述,请参阅“特性封装”侧边栏。
更多细节
某些特性只能在给定的元素上使用一次。其他特性,称为多用途特性,可以使用多次。例如,这可以用于将多个不同的安全特性应用于单个类。特性的文档将描述该特性是单用途还是多用途。
在大多数情况下,很清楚该特性适用于特定的程序元素。但是,考虑以下情况:
class Test
{
[ReturnsHResult]
public void Execute()
{}
}
在大多数情况下,该位置的特性将应用于成员函数,但该特性实际上与返回类型有关。编译器如何区分呢?
在几种情况下会发生这种情况:
- 方法与返回值
- 事件与字段或属性
- 委托与返回值
- 属性与访问器与 getter 的返回值与 setter 的值参数
对于每种情况,都有一种情况比另一种情况更常见,并且成为默认情况。要为非默认情况指定特性,必须指定特性所适用的元素:
class Test
{
[return:ReturnsHResult]
public void Execute()
{}
}
return:
表示此特性应应用于返回值。
即使没有歧义,也可以指定元素。标识符如下:
说明符 | 描述 |
assembly |
特性在程序集上 |
module |
特性在模块上 |
type |
特性在类或结构上 |
method |
特性在方法上 |
属性 |
特性在属性上 |
事件 |
特性在事件上 |
字段 |
特性在字段上 |
param |
特性在参数上 |
return |
特性在返回值上 |
应用于程序集或模块的特性必须出现在任何 using
子句之后,代码之前。
using System;
[assembly:CLSCompliant(true)]
class Test
{
Test() {}
}
此示例将 ClsCompliant
特性应用于整个程序集。在程序集中以任何文件声明的所有程序集级别特性都会被分组并附加到程序集。
要使用预定义特性,首先找到最能传达信息的构造函数。然后,编写特性,将参数传递给构造函数。最后,使用命名参数语法传递未包含在构造函数参数中的附加信息。
有关特性的更多示例,请参阅第二十九章“互操作”。
特性封装 它实际上不像描述的那样工作有几个原因,这些原因与性能有关。为了让编译器实际创建特性对象,.NET 运行时环境必须在运行,因此每次编译都必须启动环境,并且每个编译器都必须作为托管可执行文件运行。 此外,对象创建并非必需,因为我们只是要将信息存储起来。 因此,编译器会验证它可以创建对象、调用构造函数并为任何命名参数设置值。然后,特性参数会被封装成一堆二进制信息,并与对象的元数据一起存储。 |
你自己定义一个特性
要定义特性类并在运行时反射它们,还需要考虑一些其他问题。本节将讨论设计特性时需要考虑的一些事项。
编写特性时需要确定两件主要事情。第一是特性可以应用的程序元素,第二是特性将存储的信息。
特性用法
将 AttributeUsage
特性放置在特性类上可以控制特性的使用位置。特性的可能值列在 AttributeTargets
枚举器中,如下所示:
值 | 含义 |
程序集 |
程序集 |
模块 |
当前程序文件 |
类 |
类 |
结构 |
结构 |
枚举 |
枚举 |
构造函数 |
构造函数 |
方法 |
方法(成员函数) |
属性 |
属性 |
字段 |
字段 |
事件 |
事件 |
接口 |
接口 |
参数 |
方法参数 |
返回 |
方法返回值 |
委托 |
委托 |
全部 |
任何地方 |
类成员 |
类、结构、枚举、构造函数、方法、属性、字段、事件、委托、接口 |
作为 AttributeUsage
特性的一部分,可以指定其中之一,或者将它们 ORed(逻辑或)在一起。
AttributeUsage
特性还用于指定特性是单用途还是多用途。这是通过命名参数 AllowMultiple
来完成的。这样的特性看起来会是这样:
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Event,
AllowMultiple = true)]
特性参数
特性将存储的信息应分为两组:每次使用都必需的信息,以及可选项目。
每次使用都必需的信息应通过特性类的构造函数获取。这强制用户在使用特性时指定所有参数。
可选项目应实现为命名参数,这允许用户指定任何合适的选项。
如果一个特性有几种不同的创建方式,需要不同的必需信息,则可以为每种用法声明单独的构造函数。不要将单独的构造函数用作可选项目的替代。
特性参数类型
特性封装格式仅支持 .NET 运行时类型的子集,因此,只有某些类型可以用作特性参数。允许的类型如下:
bool
,byte
,char
,double
,float
,int
,long
,short
,string
object
System.Type
- 具有公共可访问性的枚举(不嵌套在非公共内容中)
- 上述类型之一的一维数组
反射特性
一旦在某些代码上定义了特性,就可以方便地查找特性的值。这是通过反射完成的。
以下代码显示了一个特性类、特性在类上的应用以及通过反射检索该特性的类。
using System;
using System.Reflection;
[AttributeUsage(AttributeTargets.Class)]
public class CodeReviewAttribute: System.Attribute
{
public CodeReviewAttribute(string reviewer, string date)
{
this.reviewer = reviewer;
this.date = date;
}
public string Comment
{
get
{
return(comment);
}
set
{
comment = value;
}
}
public string Date
{
get
{
return(date);
}
}
public string Reviewer
{
get
{
return(reviewer);
}
}
string reviewer;
string date;
string comment;
}
[CodeReview("Eric", "01-12-2000", Comment="Bitchin' Code")]
class Complex
{
}
class Test
{
public static void Main()
{
System.Reflection.MemberInfo info;
info = typeof(Complex);
object[] atts;
atts = info.GetCustomAttributes(typeof(CodeReviewAttribute));
if (atts.GetLength(0) != 0)
{
CodeReviewAttribute att = (CodeReviewAttribute) atts[0];
Console.WriteLine("Reviewer: {0}", att.Reviewer);
Console.WriteLine("Date: {0}", att.Date);
Console.WriteLine("Comment: {0}", att.Comment);
}
}
}
Main()
函数首先获取与类型 Complex
关联的类型对象。然后它加载所有属于 CodeReviewAttribute
类型的特性。如果特性的数组有任何条目,则将第一个元素强制转换为 CodeReviewAttribute
,然后打印出该值。数组中只能有一个条目,因为 CodeReviewAttribute
是单用途的。
此示例产生以下输出:
Reviewer: Eric
Date: 01-12-2000
Comment: Bitchin' Code
GetCustomAttribute()
也可以在不带类型的情况下调用,以获取对象上的所有自定义特性。
注意:“前面示例中的 CustomAttributes
”指的是存储在对象元数据通用特性部分中的特性。某些 .NET 运行时特性不作为自定义特性存储在对象上,而是被转换为对象上的元数据位。运行时反射不支持通过反射查看这些特性。此限制可能在运行时的未来版本中得到解决。