[assembly: MyAttr(1)] // 应用于程序集[module: MyAttr(2)] // 应用于模块 [type: MyAttr(3)] // 应用于类型internal sealed class SomeType<[typevar: MyAttr(4)] T> { // 应用于泛型类型变量 [field: MyAttr(5)] // 应用于字段 public Int32 SomeField = 0; [return: MyAttr(6)] // 应用于返回值 [method: MyAttr(7)] // 应用于方法 public Int32 SomeMethod( [param: MyAttr(8)] // 应用于方法参数 Int32 SomeParam) { return SomeParam; } [property: MyAttr(9)] // 应用于属性 public String SomeProp { [method: MyAttr(10)] // 应用于get访问器方法 get { return null; } } [event: MyAttr(11)] // 应用于事件 [field: MyAttr(12)] // 应用于编译器生成的字段 [method: MyAttr(13)] // 应用于编译器生成的add&&remove方法 public event EventHandler SomeEvent;} [AttributeUsage(AttributeTargets.All)]public class MyAttr : Attribute { public MyAttr(Int32 x) { }}
前面介绍了如何应用一个attribute,接下来看看attribute到底是什么?
attribute实际是一个类的实例。为了符合"公共语言规范"(CLS)的要求,attribute类必须直接或间接地从公共抽象类System.Attribute派生。C#只允许使用符合CLS规范的attribute。attribute类是可以在任何命名空间中定义的。
如前所述,attribute是类的一个实例。类必须有一个公共构造器,这样才能创建它的实例。所以,将一个attribute应用于一个目标元素时,语法类似于调用类的某个实例构造器。除此之外,语言可能支持一些特殊的语法,允许你设置于attribute类关联的公共字段或属性。比如,我们将DllImport这个attribute应用于GetVersionEx方法:[DllImport("kernel32",CharSet = CharSet.Auto, SetLastError = true)]
这一行代码语法表面上很奇怪,因为调用构造器时永远不会出现这样的语法。查阅DllImportAttbute类的文档,会发现它只接受一个String类型的参数。在这个例子中,"Kernel32"这个String类型的参数已经传给它了。构造器的参数称为"定位参数",而且是强制性的。也就是说,应用attribute时,必须指定参数。
那么,另外两个"参数"是什么?这种特殊的语法允许在DllImportAttbute对象构造好之后,设置对象的任何公共字段和属性。在这个例子中,当DllImportAttbute对象构造好,而且将"Kernel32"传给构造器之后,对象的公共实例字段CharSet和SetListError被分别设置为CharSet.Auto和true。用于设置字段或属性的"参数"被称为"命名参数"。这种参数是可选的,因为在应用attribute的一个实例时,不一定要指定命名参数。 另外,还可以将多个attribute应用于一个目标元素。将多个attribute应用一个目标元素时,attribute的顺序是无关紧要的。在C#中,可将每个attribute都封闭到一对方括号中,也可以在一对方括号中封闭多个以逗号分隔的attribute。 二、定义自己的attibude类 现在我们已经知道attribute是从System.Attribute派生的一个类的实例,并知道如何应用一个attribute。接着我们来研究下如何定制attribute。假定你是Microsoft的一位员工,并负责为枚举类型添加位标志(biit flag)支持。为此,我们,要做的第一件事情是定义一个FlagAttribute类:namespace System {public class FlagsAttribute : System.Attribute {public FlagsAttribute(){}}} 注意,FlagsAttribute类是从Attribute继承的。所以,才使FlagsAttribute类成为符合CLS要求的一个attribute。除此之外,注意类名有一个Attribute后缀;这是为了保持于标准的相容性,但并不是必须的。最后,所有的非抽象attribute都至少要包括一个公共构造器。 提示:attribute类型是一个类,但这个类应该非常简单。这个类应该只提供一个公共构造器,它接受attribute的强制性状态消息,而且这个类可以提供公共字段和属性,以接受attribute的可选状态信息。这个类不应提供任何公共方法、事件或其他成员。 三、attribute的构造器和字段/属性的数据类型 定义一个attribute类时,可定义构造器来获取参数。开发人员在应用该attribute类型的一个实例时,必须指定这些参数。除此之外,可在自己的类型中定义非静态公共字段和属性,使开发人员能够为attribute类的实例选择恰当的设置。 定义attribute类的实例构造器、字段和属性时,数据类型只能限制在一个小的子集内。具体的说,合法的数据类型只有:Boolean,Char,Byte,Sbyte,Int16,UInt16,Int32,Uint32,Int64,Uint64,Single,Double,String,Type,Object或枚举类型。除此之外,还可使用上述任意类型的一维0基数组。然而,要尽量避免使用数组,因为对于attribute类来说,如果它的构造器要获取一个数组作为参数,就会失去与CLS的相容性。 应用一个attribute时,必须传递一个编译时常量表达式,它与attribute类定义的类型相匹配。在attribute类定义一个Type参数,Type字段或者Type属性的任何地方,都必须使用C#的typeof操作符。在attribute类定义一个Object参数、Object字段或Object属性的任何地方,都可以传递一个Int32、String或者其他任何常量表达式(包括null)。如果常量表达式代表一个值类型,那么在运行时构造一个attribute的实例时,会对值类型进行装箱。以下是一个示例attribute及用法:public enum Color { Red } [AttributeUsage(AttributeTargets.All)]internal sealed class SomeAttribute : Attribute{ public SomeAttribute(String name, Object o, Type[] types) { // 'name' 引用了一个String类型 // 'o' 引用了一个合法类型(如有必要,就进行装箱) // 'types' 引用一个一维0基Type数组 }} [Some("Jeff", Color.Red, new Type[] { typeof(Math), typeof(Console) })]internal sealed class SomeType{}
逻辑上,当编译器检测到一个目标元素应用了一个attribute时,编译器会调用attribute类的构造器,向它传递任何指定的参数,从而构造attribute类的一个实例。然后,编译器会采用增强型构造器语法所指定的值,对任何公共字段和属性进行初始化。在构造并初始化好定制attribute类的对象之后,编译器会将这个attribute对象序列化到目标元素的元数据表记录项中。
提示:所谓"attribute",就是一个类的实例,它被序列化成驻留在元数据中的一个字节流。在运行时,可以对元数据中包含的字节进行反序列化,从而构造类的一个实例。实际发生的事情是:编译器在元数据中生成创建attribute类的一个实例所需的信息。每个构造器参数都采取这样的格式写入:一个1字节长度的类型ID,后跟具体的值。在对构造器参数进行"序列化"之后,编译器写入字段/属性名称,后跟一个1字节的类型ID,再跟上具体的值,从而生成指定的每个字段和属性的值。对于数组,会先保存数组元素的个数,后跟每个单独的元素。 四、检测定制的attribute 可利用反射的技术来检查attribute的存在。以后我们会完整探讨这种技术。 假定你是Microsoft的员工,负责实现Enum的Format方法(Format方法会更根据是否有FlagsAttribute输出不同的值),你会像下面这样实现它:public static String Format(Type enumType, Object value, String format) { // 枚举类型是否应用了FlagsAttrobute类型的一个实例 if(enumType.IsDefined(typeof(FlagsAttribute),false)){ //如果是,就执行代码,将值视为一个位标志枚举类型 ... }else { // 如果不是,就执行代码,将值视为一个普通枚举类型 }}
上述代码调用Type的IsDefined方法,要求系统查看枚举类型的元数据,检查是否关联了FlagsAttribute类的一个实例。如果IsDefined返回true,表面FlagsAttribute的一个实例已于枚举类型关联,Format方法会认为值包含了一个位标志集合。如果IsDefined放回false,Format方法会认为值是一个普通的枚举类型。
定义定制attribute时,也必须实现一些代码来检查某个目标上是否存在该attribute类的实例,然后执行一些逻辑分支代码。正因为能做到这一点,定制attribute才如此有用。 FCL提供了多种方式检查一个attribute的存在。我们知道所有于CLS相容的attribute都是从System.Attribute派生的。这个类定义了三个静态方法来获取与一个目标关联的attribute:IsDefined,GetCustomAttribute和GetCustomAttributes。每个方法都有几个重载版本。方法名称 | 说明 |
IsDefined | 如果至少有一个指定的Attribute派生类的实例如目标关联,就放回true。这个方法效率很高,因为它不构造(反序列化)attribute类的任何实例 |
GetCustomAttribute | 返回引用于目标的指定attribute类的一个实例。实例使用编译时指定的参数、字段和属性来构造。如果目标没有引用任何attribute类的实例,就返回null。如果目标应用了指定attribute的多个实例,就抛出异常。方法通常用于已将AllowMultiple设为false的attribute。 |
GetCustomAttributes | 返回一个数组,其中每个元素都是应用于目标的指定attribute类的一个实例。如果不为这个方法指定具体的attribute类,数组中包含的就是已应用的所有attribute的实例,不管它们是什么类。每个实例都使用编译时指定的参数、字段和属性来构造(反序列化)。如果目标没有应用任何attribute类的实例,就返回一个空数组。该方法通常用于已将AllowMultiple设为true的attribute,或者用于列出已应用的所有attribute。 |
[assembly: CLSCompliant(true)] [Serializable] [DefaultMemberAttribute("Main")] [DebuggerDisplayAttribute("Richter", Name = "Jeff", Target = typeof(Program))] public sealed class Program { [Conditional("Debug")] [Conditional("Release")] public void DoSomething() { } public Program() { } [assembly: CLSCompliant(true)] [STAThread] public static void Main() { // 显示应用于这个类型的attribute集 ShowAttributes(typeof(Program)); // 获取与类型关联的方法集 MemberInfo[] members = typeof(Program).FindMembers( MemberTypes.Constructor | MemberTypes.Method, BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public | BindingFlags.Static, Type.FilterNam
新闻热点
疑难解答