设计类型的时候可以使用各种成员来描述该类型的信息,但有时候我们可能不太愿意将一些附加信息放到类的内部,因为这样,可能会给类型本身的信息描述带来麻烦或误解。我们想为类型、属性、方法及返回值附加额外的信息,这些附加信息可以更明确的表达类及其对象成员的状态,怎么办?定制特性Attribute可以做到。
为了避免Attribute与Property翻译性误解,我们以下的讨论中将以特性表示Attribute。
细心的读者可能会发现如下类似定义:
//项目的AssemblyInfo.cs文件内有: [assembly: Guid("df510f85-e549-4999-864d-bb892545690b")] [assembly: AssemblyVersion("1.0.0.0")] //也可能会发现有些类前面也有类似的语句: [Serializable, ComVisible(true)] public sealed class String {} //在我们开发WCF项目时,定义接口契约时接口前面也有类似的语句: [ServiceContract] public interface IService {}
这些放在方括弧[]中的Serializable、ServiceContract就是.NET Framework提供的特性Attribute。它们有的作用于程序集,有的作用于类和接口,也有的作用于属性和方法等其他成员。
特性Attribute是指给声明性对象附加一些声明性描述信息,这些信息在经过编译器编译后相当于目标对象的自描述信息被编译进托管模块的元数据中,很显然,如果这些描述信息太多,会大大增加元数据的体积,这一点要注意。编译器只是将这些描述信息编译生成到元数据中,而对Attribute的“逻辑”并不关注。
前面提到的AssemblyVersion 、Serializable、ServiceContrac等都是继承于System.Attribute类,CLS要求定制Attribute必须继承于System.Attribute类,为了符合规范,所有的定制特性都要以Attribute后缀,这只是一个规范,也可以不使用此后缀,并没有强制。即使采用了后缀,为了方便编码,C#编译器也是允许在编码时省略后缀的,而VS智能提示对此也有很好的支持。
如下我们定义了一个有关国家的定制特性:
public class CountryAttribute : Attribute { public CountryAttribute(string name) { this.Name = name; } public int PlayerCount { get; set; } public string Name { get; set; } }
来看一下编译干了什么:
可以看到,定制特性就是一个普通的类,继承了System.Attribute类,没有什么特殊的地方。非抽象的定制特性类必须有至少一个公共构造函数,因为在将一个定制特性应用于其他目标元素时是以定制特性的实例起作用的。定制特性应该注意以下几点:
(1) 可以在定制特性内提供公共字段和属性,但不应该提供任何公共方法、事件等成员,像上面代码中的属性Name和PlayerCount都是被允许的。
(2) 公共实例构造函数可以有参数,也可以无参数,也可以同时提供多个构造函数。如上面的CountryAttribute类可以增加一个无参的构造函数:
public CountryAttribute() { }
(3)定义Attribute类的实例构造函数的参数、字段和属性时,只能使用以下数据类型:object,type,string,Boolean,char,byte,sbyte,Int16,int,UInt16,UInt32,Int64,UInt64,Single,double,枚举和一维0基数组。
前面的定制特性CountryAttribute可以应用于任何目标元素?如果我们希望它只应用于类类型或方法时怎么办呢?.NET Framework当然提供了这一方面的支持:System. AttributeUsageAttribute类。AttributeUsageAttribute是.NET Framework提供的一个定制特性,它主要是作用于其他定制特性来限制目标定制特性的作用目标。看一下其定义:
public sealed class AttributeUsageAttribute : Attribute { public AttributeUsageAttribute(AttributeTargets validOn); public bool AllowMultiple { get; set; } public bool Inherited { get; set; } public AttributeTargets ValidOn { get; } }
(1)该类只提供了一个公共实例构造器,其接收的参数validOn是枚举类型AttributeTargets,它指定了定制特性的作用范围,比如:程序集、模块、结构、类等。
public enum AttributeTargets { Assembly = 1, Module = 2, Class = 4, Struct = 8, Enum = 16, Constructor = 32, Method = 64, Property = 128, Field = 256, Event = 512, Interface = 1024, Parameter = 2048, Delegate = 4096, ReturnValue = 8192, GenericParameter = 16384, All = 32767, }
(2)AttributeUsageAttribute有一个附加属性AllowMultiple,它表示是否允许将定制特性的实例多次应用于同一个目标元素。
(3)AttributeUsageAttribute还有一个附加属性Inherited,它表示定制特性应用于基类时,是否将该特性同时应用于派生类及重写的的成员。
我们对CountryAttribute类进行了改造,同时定义了两个类使用定制特性CountryAttribute,如下代码:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.ReturnValue | AttributeTargets.Property, AllowMultiple = true, Inherited = true)] public class CountryAttribute : Attribute { public CountryAttribute() { } public CountryAttribute(string name) { this.Name = name; } public int PlayerCount { get; set; } public string Name { get; set; } } [Country("China")] [Country("America")] public class Sportsman { public string Name { get; set; } [Country(PlayerCount = 5)] public virtual void Play() { } } public class Hoopster : Sportsman { public override void Play() { } }
我们将CountryAttibute特性限定只能用于类、方法、方法返回值和属性:
AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.ReturnValue | AttributeTargets.Property
并且允许该定制特性的实例多次应用于同一个目标元素:
AllowMultiple = true
同时还要求将该特性不仅用于基类,也应用于派生类及重写成员:
Inherited = true
在Sportsman类我们应用了两次定制特性 [Country("China")]和[Country("America")]。对基类Sportsman及其成员方法Play()我们使用了定制特性,在其派生类Hoopster同样可以得到这些定制特性,下面讨论中我们将验证这一点。
细心的你可能会发现[Country("China")]和[Country(PlayerCount = 5)]有点相似但又有些不同,为什么?
(1) 我们在定义定制特性的时候是CountryAttribute,这里可以简写为Country。在将Country应用于某目标元素时,编译器进行编译时已经确认它是一个定制特性,接着它会在Attribute继承类中查找Country,如果没找到,则会加上Attribute后缀继续查找,再找不到,就会“啪”的一声报错了!
(2) Country("China")是在为实例构造器传递参数”China”,这没什么好解释的,问题是[Country(PlayerCount = 5)],我们并没有为County的构造函数设置参数PlayerCount啊。先来看一下在VS中编写代码时的智能提示:
这种特殊的语法将定制特性的字段和属性认定为命名参数,它允许定制特性对象构造完了之后,使用命名参数设置对象的公共字段和属性。这就提供了很大的灵活性,你可以将实例构造器的参数设为公共的字段或属性,也可以将公共的字段和属性设为私有,然后在实例构造函数处接收参数再设置它们。当然,有一点,如果使用实例构造函数,则该函数的参数都必须提供值,如果使用公共字段和属性(命名参数),则可以部分提供值,其他字段和属性可以维持默认值。建议使用属性而不是字段,可以对其进行更多的控制。
最后我们再来看一下编译器对使用了定制特性的类是干了什么?
定制特性的America和China是被写入到元数据中的。
如果仅仅是对目标元素应用了定制特性,好像意义并不大,更重要的是在应用了特性之后,我们要使用这些特性附带的信息。通常是通过反射(Reflection)配合System.Attribute的静态方法来使用特性信息。先来看一下System.Attribute的三个重要的静态方法:
IsDefined 判断指定的目标元素是否应用了System.Attribute的派生类(定制特性),它有多个重载。
GetCustomAttribute 返回应用于目标元素的与指定类型一致的特性对象,如果目标元素没有应用特性实例则返回null;如果目标元素应用了指定特性类型的多个实例,则抛出异常,它也有多个重载。
GetCustomAttributes 返回应用于目标元素的特性数组,在其重载方法中,也可以指定特性类型,它也有多个重载。
我们新定义一个定制特性:
[AttributeUsage(AttributeTargets.All)] public class TestAttribute : Attribute { }
AttributeTargets.All指出可以将该特性应用于任何目标元素。
改造一下Sportsman类:
[Country("China")] [Country("America")] public class Sportsman { public string Name { get; set; } [Country("Sports")] public virtual void Play() { Console.WriteLine("Play"); } }
对方法Play()应用了[Country("Sports")],表明了运动类型为体育运动Sports。接着我们改造Hoopster类的Play()方法:
public class Hoopster : Sportsman { public override void Play() { MemberInfo[] members = this.GetType().GetMembers(); foreach (MemberInfo member in members) { Attribute testAttr = Attribute.GetCustomAttribute(member, typeof(TestAttribute)); if (testAttr != null && testAttr is TestAttribute) { Console.WriteLine(((TestAttribute)testAttr).Message); } if (Attribute.IsDefined(member, typeof(CountryAttribute))) { Attribute[] attributes = Attribute.GetCustomAttributes(member); foreach (Attribute item in attributes) { CountryAttribute attr = item as CountryAttribute; if (attr != null) { Console.WriteLine(string.Format("运动类型:{0} 运动员人数:{1}", attr.Name, attr.PlayerCount)); } } } } } }
获取当前对象的全部成员后,接着循环每一个成员。
无论是Sportsman类还是Hoopster类都没有应用TestAttribute特性,所以testAttr将一直保持为null。
接着我们应用Attribute.IsDefined方法判断每一个成员是否应用了定制特性CountryAttribute。如果应用了,接着获取所有应用于该成员的特性。然后循环特性,如果特性是定制特性CountryAttribute类型,则打印出我们定义的运动类型和运动员人数。运行结果如下:
很明显,我们对基类Sportsman方法Play的定义[Country("Sports")]已经影响到了子类Hoopster,这验证了我们前面所说的Inherited = true的作用。由于我们未给PlayerCount赋值,所以它依然是默认值0。
接下来我们继续改造子类Hoopster的方法Play:
[Country("Ball", PlayerCount = 5)] public override void Play() { //... }
再来看看它的运行结果:
这一次不仅打印出了Sports/0,还打印出了Ball/5,这是因为我们为子类Hoopster的Play方法应用了[Country("Ball", PlayerCount = 5)]特性,方法Play不仅得到了基类的特性信息,也拥有自己的特性信息。