CLR via C#, 4th -- 【基本类型】 -- 第18章定制特性
18.1 使用定制特性
关于自定义特性,首先要知道它们只是将一些附加信息与某个目标元素关联起来的方式。
.NET Framework类库(FCL)定义的定制特性
- 将DIllmport特性应用于方法,告诉CLR该方法的实现位于指定DLL的非托管代码中。
- 将Serializable特性应用于类型,告诉序列化格式化器一个实例的字段可以序列化和反序列化。
- 将AssemblyVersion特性应用于程序集,设置程序集的版本号。
- 将Flags特性应用于枚举类型,枚举类型就成了位标志(bit flag)集合。
在C#中,为了将定制特性应用于目标元素,要将特性放置于目标元素前的一对方括号中。
using System; using System.Runtime.InteropServices; [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] internal sealed class OSVERSIONINFO { public OSVERSIONINFO() { OSVersionInfoSize = (UInt32) Marshal.SizeOf(this); } public UInt32 OSVersionInfoSize = 0; public UInt32 MajorVersion = 0; public UInt32 MinorVersion = 0; public UInt32 BuildNumber = 0; public UInt32 PlatformId = 0; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] public String CSDVersion = null; } internal sealed class MyClass { [DllImport("Kernel32", CharSet = CharSet.Auto, SetLastError = true)] public static extern Boolean GetVersionEx([In, Out] OSVERSIONINFO ver); }
CLR允许将特性应用于可在文件的元数据中表示的几乎任何东西。不过,最常应用特性的还是以下定义表中的记录项:TypeDef(类、结构、枚举、接口和委托),MethodDef(含构造器),ParamDef,FieldDef,PropertyDef,EventDef,AssemblyDef和ModuleDef.更具体地说,C#只允许将特性应用于定义以下任何目标元素的源代码:程序集、模块、类型(类、结构、枚举、接口、委托)、字段、方法(含构造器)、方法参数、方法返回值、属性、事件和泛型类型参数。
应用特性时,C#允许用一个前缀明确指定特性要应用于的目标元素。以下代码展示了所有可能的前缀。许多时候,即使省略前缀,编译器也能判断特性要应用于什么目标元素(就像上例展示的那样)。但在其他时候,必须指定前缀向编译器清楚表明我们的意图。
using System; [assembly : SomeAttr] // Applied to assembly [module : SomeAttr] // Applied to module [type: SomeAttr] // Applied to type internal sealed class SomeType<[typevar: SomeAttr] T> { // Applied to generic type variable [field: SomeAttr] // Applied to field public Int32 SomeField = 0; [return : SomeAttr] // Applied to return value [method: SomeAttr] // Applied to method public Int32 SomeMethod( [param: SomeAttr] // Applied to parameter Int32 SomeParam) { return SomeParam; } [property: SomeAttr] // Applied to property public String SomeProp { [method: SomeAttr] // Applied to get accessor method get { return null; } } [event: SomeAttr] // Applied to event [field: SomeAttr] // Applied to compilergenerated field [method : SomeAttr] // Applied to compiler generated add & remove methods public event EventHandler SomeEvent; }
定制特性其实是一个类型的实例。为了符合“公共语言规范”(CLS)的要求,定制特性类必须直接或间接从公共抽象类System.Attribute派生。C#只允许符合CLS规范的特性。
将特性应用于源代码中的目标元素时,C#编译器允许省略Attribute后级以减少打字量,并提升源代码的可读性。本章许多示例代码都利用了C#提供的这一便利。
特性是类的实例。类必须有公共构造器才能创建它的实例。所以,将特性应用于目标元素时,语法类似于调用类的某个实例构造器。除此之外,语言可能支持一些特殊的语法,允许设置与特性类关联的公共字段或属性。
[DllImport("Kernel32", CharSet = CharSet.Auto, SetLastError = true)]
构造器的参数称为定位参数(positional parameter),而且是强制性的:也就是说,应用特性时必须指定参数。
用于设置字段或属性的“参数”称为命名参数(named parameter).这种参数是可选的,因为在应用特性的实例时不一定要指定参数。
还可将多个特性应用于一个目标元素。在C#中,既可将每个特性都封闭到一对方括号中,也可在一对方括号中封闭多个以逗号分隔的特性。如果特性类的构造器不获取参数,那么圆括号可以省略。最后,就像前面说到的那样,Attribute后缀也是可选的。
[Serializable][Flags]
[Serializable, Flags]
[FlagsAttribute, SerializableAttribute]
[FlagsAttribute()][Serializable()]
18.2定义自己的特性类
namespace System { public class FlagsAttribute : System.Attribute { public FlagsAttribute() { } } }
FlagsAttribute类从Attribute继承。这使FlagsAttribute类成为符合CLS规范的定制特性。此外,注意类名有Attribute后缀;这是为了保持与标准的相容性,但这并不是必须的。最后,所有非抽象特性至少要包含一个公共构造器。
应将特性想像成逻辑状态容器。也就是说,虽然特性类型是一个类,但这个类应该很简单。应该只提供一个公共构造器来接受特性的强制性(或定位性)状态信息,而且这个类可以提供公共字段和属性,以接受特性的可选(或命名)状态信息,类不应提供任何公共方法、事件或其他成员。
为了告诉编译器这个特性的合法应用范围,需要向特性类应用System.AttributeUsageAttribute类的实例。
namespace System { [AttributeUsage(AttributeTargets.Enum, Inherited = false)] public class FlagsAttribute : System.Attribute { public FlagsAttribute() { } } }
新版本将AttributeUsageAttribute的实例应用于特性。毕竟,特性类型本质上还是类,而类是可以应用特性的。AttributeUsage特性是一个简单的类,可利用它告诉编译器定制特性的合法应用范围。
[Serializable] [AttributeUsage(AttributeTargets.Class, Inherited=true)] public sealed class AttributeUsageAttribute : Attribute { internal static AttributeUsageAttribute Default = new AttributeUsageAttribute(AttributeTargets.All); internal Boolean m_allowMultiple = false; internal AttributeTargets m_attributeTarget = AttributeTargets.All; internal Boolean m_inherited = true; // This is the one public constructor public AttributeUsageAttribute(AttributeTargets validOn) { m_attributeTarget = validOn; } internal AttributeUsageAttribute(AttributeTargets validOn, Boolean allowMultiple, Boolean inherited) { m_attributeTarget = validOn; m_allowMultiple = allowMultiple; m_inherited = inherited; } public Boolean AllowMultiple { get { return m_allowMultiple; } set { m_allowMultiple = value; } } public Boolean Inherited { get { return m_inherited; } set { m_inherited = value; } } public AttributeTargets ValidOn { get { return m_attributeTarget; } } }
AttributeUsageAttribute类有一个公共构造器,它允许传递位标志(bit flag)来指明特性的合法应用范围。
[Flags, Serializable] public enum AttributeTargets { Assembly = 0x0001, Module = 0x0002, Class = 0x0004, Struct = 0x0008, Enum = 0x0010, Constructor = 0x0020, Method = 0x0040, Property = 0x0080, Field = 0x0100, Event = 0x0200, Interface = 0x0400, Parameter = 0x0800, Delegate = 0x1000, ReturnValue = 0x2000, GenericParameter = 0x4000, All = Assembly | Module | Class | Struct | Enum | Constructor | Method | Property | Field | Event | Interface | Parameter | Delegate | ReturnValue | GenericParameter }
AttributeUsageAttribute类提供了两个附加的公共属性,即AllowMultiple和Inherited可在向特性类应用特性时选择设置这两个属性。
少数几个特性确实有必要多次应用于同一个目标。FCL特性类ConditionalAttribute允许将它的多个实例应用于同一个目标元素。不将AllowMultiple明确设为true,特性就只能向选定的目标元素应用一次。
AttributeUsageAttribute的另一个属性是Inherited,它指出特性在应用于基类时,是否同时应用于派生类和重写的方法。
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited=true)] internal class TastyAttribute : Attribute { } [Tasty][Serializable] internal class BaseType { [Tasty] protected virtual void DoSomething() { } } internal class DerivedType : BaseType { protected override void DoSomething() { } }
.NET Framework只认为类、方法、属性、事件、字段、方法返回值和参数等目标元素是可继承的。所以,定义特性类型时,只有在该特性应用于上述某个目标的前提下,才应该将Inherited设为true。
定义自己的特性类时,如果忘记向自己的类应用AttributeUsage特性,编译器和CLR将假定该特性能应用于所有目标元素,向每个目标元素都只能应用一次,而且可继承。
18.3 特性构造器和字段/属性数据类型
定义特性类的实例构造器、字段和属性时,可供选择的数据类型并不多。具体地说,只允许Boolean,Char,Byte,SByte,Int16,UInt16,Int32,UInt32,Int64,UInt64,Single,Double,String,Type,Object或枚举类型。此外,可使用上述任意类型的一维0基数组。
应用特性时必须传递一个编译时常量表达式,它与特性类定义的类型匹配。在特性类定义了一个Type参数、Type字段或者Type属性的任何地方,都必须使用CH typeof操作符(如下例所示)。在特性类定义了一个Object参数、Object字段或者Object属性的任何地方,都可传递一个Int32、String或其他任何常量表达式(包括null),如果常量表达式代表值类型,那么在运行时构造特性的实例时会对值类型进行装箱。
using System; internal enum Color { Red } [AttributeUsage(AttributeTargets.All)] internal sealed class SomeAttribute : Attribute { public SomeAttribute(String name, Object o, Type[] types) { // 'name' refers to a String // 'o' refers to one of the legal types (boxing if necessary) // 'types' refers to a 1dimension, 0based array of Types } } [Some("Jeff", Color.Red, new Type[] { typeof(Math), typeof(Console) })] internal sealed class SomeType { }
18.4 检测定制特性
在定义定制特性时,也必须实现一些代码来检测某些目标上是否存在该特性类的实例,然后执行一些逻辑分支代码。这样定制特性才能真正发挥作用。
System.Reflection.CustomAttributeExtensions类定义的扩展方法。该类定义了三个静态方法来获取与目标关联的特性:IsDefined,GetCustomAttributes和GetCustomAttribute。
如果只想判断目标是否应用了一个特性,那么应该调用IsDefined,因为它比另两个方法更高效。但我们知道,将特性应用于目标时,可以为特性的构造器指定参数,并可选择设置字段和属性。使用IsDefined不会构造特性对象,不会调用构造器,也不会设置字段和属性。
TABLE 18-1 System.Reflection.CustomAttributeExtensions’ Methods That Reflect over Metadata Looking for Instances of CLS-Compliant Custom Attributes
|
Method |
Description |
|
IsDefined |
如果至少有一个指定的Attribute派生类的实例与目标关联,就返回true。这个方法效率很高,因为它不构造(反序列化)特性类的任何实例 |
|
GetCustomAttributes |
返回应用于目标的指定特性对象的集合。每个实例都使用编译时指定的参数、字段和属性来构造(反序列化)。如果目标没有应用指定特性类的实例,就返回一个空集合。该方法通常用于已将AllowMultiple设为true的特性,或者用于列出已应用的所有特性 |
|
GetCustomAttribute |
返回应用于目标的指定特性类的实例。实例使用编译时指定的参数、字段和属性来构造(反序列化)。如果目标没有应用指定特性类的实例,就返回nullo如果目标用了指定特性的多个实例就 t setemetonAmunus Mth Exepion异常。该方法通常用于已将AllowMultiple设为false的特性 |
将一个类传给IsDefined,GetCustomAttribute或者GetCustomAttributes方法时,这些方法会检测是否应用了指定的特性类或者它的派生类。如果只是想搜索一个具体的特性类,应针对返回值执行一次额外的检查,确保方法返回的正是想搜索的类。还可考虑将自己的特性类定义成sealed,减少可能存在的混淆,并避免执行这个额外的检查。
using System; using System.Diagnostics; using System.Reflection; [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() { } [CLSCompliant(true)] [STAThread] public static void Main() { // Show the set of attributes applied to this type ShowAttributes(typeof(Program)); // Get the set of methods associated with the type var members = from m in typeof(Program).GetTypeInfo().DeclaredMembers.OfType<MethodBase>() where m.IsPublic select m; foreach (MemberInfo member in members) { // Show the set of attributes applied to this member ShowAttributes(member); } } private static void ShowAttributes(MemberInfo attributeTarget) { var attributes = attributeTarget.GetCustomAttributes<Attribute>(); Console.WriteLine("Attributes applied to {0}: {1}", attributeTarget.Name, (attributes.Count() == 0 ? "None" : String.Empty)); foreach (Attribute attribute in attributes) { // Display the type of each applied attribute Console.WriteLine(" {0}", attribute.GetType().ToString()); if (attribute is DefaultMemberAttribute) Console.WriteLine(" MemberName={0}", ((DefaultMemberAttribute) attribute).MemberName); if (attribute is ConditionalAttribute) Console.WriteLine(" ConditionString={0}", ((ConditionalAttribute) attribute).ConditionString); if (attribute is CLSCompliantAttribute) Console.WriteLine(" IsCompliant={0}", ((CLSCompliantAttribute) attribute).IsCompliant); DebuggerDisplayAttribute dda = attribute as DebuggerDisplayAttribute; if (dda != null) { Console.WriteLine(" Value={0}, Name={1}, Target={2}", dda.Value, dda.Name, dda.Target); } } Console.WriteLine(); } }
18.5 两个特性实例的相互匹配
除了判断是否向目标应用了一个特性的实例,可能还需要检查特性的字段来确定它们的值。一个办法是老老实实写代码检查特性类的字段值。但System.Attribute重写了Object的Equals方法,会在内部比较两个对象的类型。不一致会返回false。
System.Attribute还公开了虚方法Match,可重写它来提供更丰富的语义。Match的默认实现只是调用Equal方法并返回它的结果。
[Flags] internal enum Accounts { Savings = 0x0001, Checking = 0x0002, Brokerage = 0x0004 } [AttributeUsage(AttributeTargets.Class)] internal sealed class AccountsAttribute : Attribute { private Accounts m_accounts; public AccountsAttribute(Accounts accounts) { m_accounts = accounts; } public override Boolean Match(Object obj) { // If the base class implements Match and the base class // is not Attribute, then uncomment the following line. // if (!base.Match(obj)) return false; // Since 'this' isn't null, if obj is null, // then the objects can't match // NOTE: This line may be deleted if you trust // that the base type implemented Match correctly. if (obj == null) return false; // If the objects are of different types, they can't match // NOTE: This line may be deleted if you trust // that the base type implemented Match correctly. if (this.GetType() != obj.GetType()) return false; // Cast obj to our type to access fields. NOTE: This cast // can't fail since we know objects are of the same type AccountsAttribute other = (AccountsAttribute) obj; // Compare the fields as you see fit // This example checks if 'this' accounts is a subset // of others' accounts if ((other.m_accounts & m_accounts) != m_accounts) return false; return true; // Objects match } public override Boolean Equals(Object obj) { // If the base class implements Equals, and the base class // is not Object, then uncomment the following line. // if (!base.Equals(obj)) return false; // Since 'this' isn't null, if obj is null, // then the objects can't be equal // NOTE: This line may be deleted if you trust // that the base type implemented Equals correctly. if (obj == null) return false; // If the objects are of different types, they can't be equal // NOTE: This line may be deleted if you trust // that the base type implemented Equals correctly. if (this.GetType() != obj.GetType()) return false; // Cast obj to our type to access fields. NOTE: This cast // can't fail since we know objects are of the same type AccountsAttribute other = (AccountsAttribute) obj; // Compare the fields to see if they have the same value // This example checks if 'this' accounts is the same // as other's accounts if (other.m_accounts != m_accounts) return false; return true; // Objects are equal } // Override GetHashCode since we override Equals public override Int32 GetHashCode() { return (Int32) m_accounts; } } [Accounts(Accounts.Savings)] internal sealed class ChildAccount { } [Accounts(Accounts.Savings | Accounts.Checking | Accounts.Brokerage)] internal sealed class AdultAccount { } public sealed class Program { public static void Main() { CanWriteCheck(new ChildAccount()); CanWriteCheck(new AdultAccount()); // This just demonstrates that the method works correctly on a // type that doesn't have the AccountsAttribute applied to it. CanWriteCheck(new Program()); } private static void CanWriteCheck(Object obj) { // Construct an instance of the attribute type and initialize it // to what we are explicitly looking for. Attribute checking = new AccountsAttribute(Accounts.Checking); // Construct the attribute instance that was applied to the type Attribute validAccounts = obj.GetType().GetCustomAttribute<AccountsAttribute>(false); // If the attribute was applied to the type AND the // attribute specifies the "Checking" account, then the // type can write a check if ((validAccounts != null) && checking.Match(validAccounts)) { Console.WriteLine("{0} types can write checks.", obj.GetType()); } else { Console.WriteLine("{0} types can NOT write checks.", obj.GetType()); } } }
18.6 检测定制特性时不创建从Attribute派生的对象
可用System.Reflection.CustomAttributeData类在查找特性的同时禁止执行特性类中的代码。该类定义了静态方法GetCustomAttributes来获取与目标关联的特性。方法有4个重载版本,分别获取一个Assembly,Module,ParameterInfo和Memberlnfo,该类在System.Reflection命名空间(将在第23章“程序集加载和反射”讨论)中定义。
通常,先用Assembly的静态方法ReflectionOnlyLoad(也在第23章讨论)加载程序集,再用CustomAttributeData类分析这个程序集的元数据中的特性。简单地说,ReflectionOnlyLoad以特殊方式加载程序集,期间会禁止CLR执行程序集中的任何代码;其中包括类型构造器。
CustomAttributeData的GetCustomAttributes方法是一个工厂(factory)方法。也就是说,调用它会返回一个IList<CustomAttributeData>类型的对象,其中包含了由CustomAttributeDats对象构成的集合。集合中的每个元素都是应用于指定目标的一个定制特性。可查询每个CustomAttributeData对象的只读属性,判断特性对象如何构造和初始化。具体地说,Constructor属性指出构造器方法将如何调用。ConstructorArguments属性以一个IList<CustomAttributeTypedArgument>实例的形式返回将传给这个构造器的实参。而NamedArguments属性以一个IList<CustomAttributeNamedArgument>实例的形式,返回将设置的字段/属性。注意,之所以说“将”,是因为不会实际地调用构造器和set访问器方法。禁止执行特性类的任何方法增强了安全性。
using System; using System.Diagnostics; using System.Reflection; using System.Collections.Generic; [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() { } [CLSCompliant(true)] [STAThread] public static void Main() { // Show the set of attributes applied to this type ShowAttributes(typeof(Program)); // Get the set of methods associated with the type MemberInfo[] members = typeof(Program).FindMembers( MemberTypes.Constructor | MemberTypes.Method, BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public | BindingFlags.Static, Type.FilterName, "*"); foreach (MemberInfo member in members) { // Show the set of attributes applied to this member ShowAttributes(member); } } private static void ShowAttributes(MemberInfo attributeTarget) { IList<CustomAttributeData> attributes = CustomAttributeData.GetCustomAttributes(attributeTarget); Console.WriteLine("Attributes applied to {0}: {1}", attributeTarget.Name, (attributes.Count == 0 ? "None" : String.Empty)); foreach (CustomAttributeData attribute in attributes) { // Display the type of each applied attribute Type t = attribute.Constructor.DeclaringType; Console.WriteLine(" {0}", t.ToString()); Console.WriteLine(" Constructor called={0}", attribute.Constructor); IList<CustomAttributeTypedArgument> posArgs = attribute.ConstructorArguments; Console.WriteLine(" Positional arguments passed to constructor:" + ((posArgs.Count == 0) ? " None" : String.Empty)); foreach (CustomAttributeTypedArgument pa in posArgs) { Console.WriteLine(" Type={0}, Value={1}", pa.ArgumentType, pa.Value); } IList<CustomAttributeNamedArgument> namedArgs = attribute.NamedArguments; Console.WriteLine(" Named arguments set after construction:" + ((namedArgs.Count == 0) ? " None" : String.Empty)); foreach(CustomAttributeNamedArgument na in namedArgs) { Console.WriteLine(" Name={0}, Type={1}, Value={2}", na.MemberInfo.Name, na.TypedValue.ArgumentType, na.TypedValue.Value); } Console.WriteLine(); } Console.WriteLine(); } }
18.7 条件特性类
应用了System.Diagnostics.ConditionalAttribute的特性类称为条件特性类。
//#define TEST #define VERIFY using System; using System.Diagnostics; [Conditional("TEST")][Conditional("VERIFY")] public sealed class CondAttribute : Attribute { } [Cond] public sealed class Program { public static void Main() { Console.WriteLine("CondAttribute is {0}applied to Program type.", Attribute.IsDefined(typeof(Program), typeof(CondAttribute)) ? "" : "not "); } }
编译器如果发现向目标元素应用了CondAttribute的实例,那么当含有目标元素的代码编译时,只有在定义TEST或VERIFY符号的前提下,编译器才会在元数据中生成特性信息。不过,特性类的定义元数据和实现仍存在于程序集中。
浙公网安备 33010602011771号