CLR.via.C#(第三版) 学习笔记 定制attribute

利用定制attribute,可以声明性地为自己代码构造添加注解,从而实现一些特殊功能。定制attribute允许将定义的信息应用于几乎每一个元数据表记录项。这种可扩展的元数据信息能在运行时查询,从而动态改变的执行方式,使用各种.NET Framework技术时,会发现它们都利用了定制attribute,目的是方便开发者在代码表达他们的意图,任何.NET Framework开发人员都有必要对定制attribute有一个牢靠的掌握。

自定义是将一些附加信息与某个目标元素关联起来的方式,编译器在托管模块的元素据中生成这些额外的信息,大多数attribute对编译器来说没有意义;编译器只是在源代码中检测attribute,并生成对应的元数据。

  • 将DLLImport attribute应用于方法,告诉CLR该方法的实现位于指定DLL的非托管代码中。
  • 将Serializable attribute应用于类型,告诉序列化格式化器(serialization formatter)一个实例的字段可以序列化和反序列化。
  • 将AssemblyVersion attribute应用于程序集,设置程序集的版本号。
  • 将Flags attribute应用于枚举类型,将枚举类型作为一个位标志(bit flag)集合使用。

为了将一个定制attribute应用于一个目标元素,需要将attribute放置于目标元素前面的一对方括号中。

using System;
using System.Runtime.InteropServices;
namespace TestProject
{
    [StructLayout(LayoutKind.Sequential,CharSet=CharSet.Auto)]
    internal sealed class OSVERSIONINFO
    {
        public OSVERSIONINFO()
        {
        }
        public UInt32 OSVersionInfoSize = 0;
        public UInt32 MajorVersion = 0;
        public UInt32 MinorVersion = 0;
        public UInt32 BuildNumber = 0;
        public UInt32 Platformd = 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);
    }

 

在这里,StructLayout attribute应用于OSVERSIONINFO类,MarshalAs attribute应用于CSVersion字段,DllImport attribute应用于GetVersionEx方法,而,In和Out attributey应用于GetVersionEx方法的ver参数。每种编程语言都定义了将一个定制attribute应用于目标元素时采用的语法。Basic .Net要求使用的是<>。

 

CLR允许将attribute应用于可在文件的元数据中表示的几乎所有元素。不过最常应用attribute的还是以下定义表中的记录项:

  • TypeDef(类,结构,枚举,接口和委托),
  • Method(含构造器),ParamDef,FieldDef,PropertyDef,EventDef,AssemblyDef和ModuleDef

应用一个attribute时,C#允许用一个前缀明确指定attribute要应用于的目标元素。以下代码展示了所有可能的前缀。在许多情况下,即使省略前缀,编译器一样能判断一个attribute要应用于的目标元素(就像上例展示的那样)。但在另一些情况下,必须指定前缀向编译器清楚表明我们的意图。

 

attribute到底是什么,定制attribute实际是一个类型的实例,为了符合“公共语言规范(CLS)”的要求,定制attribute类必须直接或间接地从公共抽象类System.Attribute派生。C#只允许使用符合CLS规范的attribute.检查. Net Fremework SDK文档,会发现定义了以下类:

  • StructLayoutAttribute,
  • MarshalAsAttribute,
  • DllImportAttribute,
  • InAttribute
  • OutAttribute

所有这些类碰巧都是在System.Runtime.InteropServices命名空间中定义的。但是,attribute类可以在任何命名空间中定义。进一步检查会发现,所有的这些类都从System.Attribute派生,所有符合CLS规范的attribute类都肯定是从这个派生类的。

 

attribute是类的一个实例,类必须有一个公共构造器,这样才能创建它的实例。所有将一个attribute应用于一个目标元素时,语法类似于调用类的某个实例构造器。除此之外,语言可能支持一些特殊的语法,允许设置于attribute类关联的公共字段或属性(property)。在前面的例子中,将DllImport这个attribute应用于GetVersionEx方法:

[DllImport("Kernel32",CharSet = CharSet.Auto, SerLastError = true)]

这一行代码的语法表面上看很奇怪,因为调用构造器时,永远不会使用这样的语法。查阅DllImportAttribute类的文档,会发现它的构造器要求接受单个String参数。在这个例子中,“Kernel32”将传给这个参数,构造器的参数称为“定位参数”(positional parameter),而且是强制性的;也就是说,在应用attribute时,参数是必须指定。

这样的特殊语法允许在DllImportAttribute对象构造好之后,设置对象的任何公共字段或属性,在这个例子中,当DllImportAttribute对象构造好,而且将“Kernel32”传递给构造器之后,对象的公共实例字段CharSet和SetLastError被分别设置为CharSet.Auto和true.

用于设置字段或属性的“参数”称为“命名参数”(named parameter)。这种参数是可选的,因为在应用attribute的一个实例时,不一定要指定参数。稍后解释什么到导致DllImportAttribute类的一个实例被实际的构造。

 

定义自己的attribute类

 

假定要为枚举类型添加位标志(bit flag)支持。为此,要做的第一件事情是定义一个FlagsAttribute类

    public class FlagsAttribute : System.Attribute
    {
        public FlagsAttribute()
        { 
        }
    }

 注意,FlagsAttribute类是从Attribute类继承的,正因如此,才使FlagsAttribute类成为符合CLS要求的一个定制attribute。除此之外,类名有一个Attribute后缀,这是为了保持与标准的相容性,但并不是必须的,最后所有非抽象attribute都至少要包含一个公共构造器,在上述代码中,FlagsAttribute构造器非常简单,它不获取任何参数,也不做任何事情。

 

FlagsAttribute类的实例能应用于任何目标元素。但事实上, 这个attribute应该只能应用于枚举类型,将这个attribute应用于属性或方法是没意义的,为了告诉编译器这个attribute的合法应用范围,需要向attribute类应用System.AttributeUsageAttribute类的一个实例。下面是新代码:

    [AttributeUsage(AttributeTargets.Enum,Inherited=false)]
    public class FlagsAttribute : System.Attribute
    {
        public FlagsAttribute()
        { 
        }
    }

 在这个新版本中,我将AttributeUsageAttribute的一个实例应用于该attribute。毕竟,attribute类型本质是一个类,而类是可以应用attribute的,AttributeUsage attribute是一个简单的类,可利用它告诉编译器定制attribute的合法应用范围,所有编译器都内建了对这个attribute的支持,并会在定制attribute应用于一个无效目标时报错,在这个例子中,AttributeUsage attribute指出,Flags attribute的实例只能应用于枚举类型的目标。

 

由于所有attribute不过是类型,所以AttributeUsageAtrreibute类理解起来很容易,以下是该类的FCL源代码:

    [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;

        public AttributeUsageAttribute(AttributeTargets validOn)
        {
            m_attributeTarget = validOn;
        }

        internal AttributeUsageAttribute(AttributeTargets validOn, Boolean allowMultipie, Boolean inherited)
        {
            m_attributeTarget = validOn;
            m_allowMultiple = allowMultipie;
            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)来指明attribute的合法应用范围。System.AttributeTargets枚举类型在FCL中是像下面这样定义的:

    [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。向attribute类应用attribute时,可选择设置这两个属性。

对于大多数attribute,将它们多次应用于一个目标没有意义的。例如,将Flags或者Serializable attribute多次应用于一个目标,不会有任何好处,事实上,如果试图使用,编译器会报告错误。

    [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()
        {
        }
    }

 在上述代码中,DerivedType及其DoSomething方法都被视为Tasty,因为TastyAttribute类被标记为可继承(inherited)。然而,DerivedType不是可序列化的,因为FCL的SerializableAttribute类被标记为一个不可继承的attribute。

注意:.NET Framework只认为类,方法,属性,事件爱你,字段,方法返回值和参数等目标元素是可继承的,所以,定义一个attribute类型时,只有在该attribute应用于上述目标的前提下,才应该讲Inherited设为true。注意,可继承attribute不会造成在托管模块中为派生类型生成额外的元数据。

注意:定义自己的attribute类时,如果忘记向自己的类应用一个AttributeUsageAttribute时,编译器和CLR将假定该attribute能应用于所有目标元素,向每个目标元素都只能应用一次,而且是可继承的,这些假定模仿了AttributeUsageAttribute类中的默认字段值。

 

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类定义的类型相匹配。在attibute类定义了一个Type参数,Type字段或者Type属性的地方,都必须使用C#的typeof操作符,在attribute类定义了一个Object参数,Object字段或者Object属性的任何地方,都可以传递一个Int32,String或者其他任何常量表达式(包括Null)。如果常量表达式代表一个值类型,那么在运行时构造attribute的一个实例时,会对值类型进行装箱,以下是一个示例attribute及其用法:

    internal 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类是没有用处的,当然,你可以定义自己想要的所有attribute类,并应用自己想要的所有实例,但是,这样做除了在程序集中生成额外的元数据之外,没有其他任何意义。应用程序代码的行为不会有任何改变。

定义定制attribute时,也必须实现一些代码来检查某些目标上是否存在该attribute类的实例,然后执行一些逻辑分支代码,正因为能做到这一点,定制attribute才如此有用!

FCL提供了多种方式检查一个attribute的存在,如果通过一个System.Type对象检查一个attribute的存在,可以调用IsDefined方法。但某些时候,需要检查除了类型之外的其他目标(比如程序集,模块或方法)上的attribute。为简化讨论,让我们将重点集中在System.Attribute类定义的方法上面,我们知道,所有与CLS相容的attribute都是从System.Attribute派生的,这个类定义了三个静态方法来获取与一个目标关联的attribute:IsDefined,GetCustomAttributes和GetCustomAttribute。每个方法都有几个重载版本。例如,每个方法都有一个版本能操作类型成员(类,结构,枚举,接口,委托,构造器,方法,属性,字段,事件和返回类型),参数,模块,和程序集,还有一些版本允许你告诉系统遍历继承层次结构,在结构中包含继承的attribute。下面简要总结每个方法的用途,它们在元素局上反射以查找于CLS相容的定制attribute类型的实例。

IsDefined : 如果至少有一个指定的Attribute派生类的实例与目标关联,就返回true。这个方法效率很高,因为它不构造(反序列化)attribute类的任何实例。

GetCustomAttribute : 返回一个数组,其中每个元素都是应用于目标的指定attribute类的一个实例,如果不为这个方法指定具体的attribute类,数组中包含的就是已应用的所有的attribute的实例,不管它们是什么类,每个实例都使用编译时指定的参数,字段和属性来构造(反序列化)。如果目标元素没有应用任何attribute类的实例,就返回一个空数组,该方法通常用于已将AllowMultiple设为true的attribute,或者用于列出所有已应用的所有attribute

 

GetCustomAttribute : 返回应用于目标的指定attribute类的一个实例,实例使用编译时指定的参数,字段,和属性,来构造(反序列化)。如果目标没有应用任何attribute类的实例,就返回null。如果目标应用了指定attribute的多个实例,就抛出一个System.Reflection.AmbiguousMatchExcekption异常。该方法通常用于将AllowMultiple设为false的attribute。

若果只是想知道一个attribute是否已应用于一个目标,那么应该调用IsDefined,因为它的效率要比另外两个方法高很多。但我们知道,将一个attribute应用于一个目标时,可以为attribute的构造器指定参数,并可选择设置字段和属性,使用IsDefined不会构造一个attribute对象,不会调用它的构造器,也不会设置它的字段和属性。

要构造一个attribute对象,必须调用GetCustomAttribute或者GetCustomAttribute方法。每次调用者两个方法,都会构造指定attribute类型的新实例,并根据源代码中指定的值来设置每个实例的字段和属性,这两个方法返回的都是一个引用,指向完全构造好的attribute类的实例。

调用上述任何一个方法时,它们在内部必须扫描托管模块的元数据,执行字符串比较来定位指定的定制attribute类,显然,这些操作会耗费一定的时间,加入对性能要求比较苛刻,可以考虑缓存这些方法的调用结果,而不是反复调用它们来请求相同的信息。

System.Reflection命名空间定义了几个类允许你检查一个模块的元数据的内容,这些类包括Assembly,Module,ParametereInfo,MemberInfo,Type,MethoodInfo,ConstructorInfo,FieldInfo,EventInfo,PropertyInfo及其各自的*Builder类,所有这些类还提供了IsDefined和GetCustomAttributes方法,但是,只有System.Attribute提供了非常方便的GetCustomAttribute方法

 

反射类提供的那个版本的GetCustomAttributes方法返回的是由Object实例构成的一个数组(Object[]),而不是又Attribute实例构成的一个数组(Attribute[])。这是由于反射类型能返回不相容于CLS规范的attribute类的对象,不过,非CLS相容的attribute是非常稀少的。

还有一件事情需要注意:将一个类传给IsDefined, GetCustomAttribute或者GetCustomAttributes时,这些方法会搜索指定的attribute类或者它的派生类的应用,如果代码要搜索一个具体的attribute类,应该针对返回值执行一次格外的检查,确保这些方法返回的正是想搜索的类,还可考虑将自己的attribute类定义成sealed,减少可能存在的混淆,并避免这个额外的检查。

以下实例代码列出来一个类型中定义的所有方法,并显示应用于每个方法的attribute。代码

 

[Serializable]
    [DefaultMember("Main")]
    [DebuggerDisplay("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()
        {
            //显示应用于这个类型的attribute集
            ShowAttributes(typeof(Program));

            //获取与类型关联的方法集
            MemberInfo[] members = typeof(Program).FindMembers(
                MemberTypes.Constructor | MemberTypes.Method,
                BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public | BindingFlags.Static,
                Type.FilterName,
                "*");

            foreach (MemberInfo member in members)
            { 
                //显示应用于这个成员的attribute集
                ShowAttributes(member);
            }
            Console.ReadKey();

        }

        private static void ShowAttributes(MemberInfo attributeTarget)
        {
            Attribute[] attributes = Attribute.GetCustomAttributes(attributeTarget);

            Console.WriteLine("Attributes applied to {0} : {1}",
                attributeTarget.Name, (attributes.Length == 0 ? "None" : String.Empty));

            foreach (Attribute attribute in attributes)
            { 
                //显示已应用的每个attribute的类型
                Console.WriteLine(" {0}", attribute.GetType().ToString());

                if (attribute is DefaultMemberAttribute)
                    Console.WriteLine(" MemberName = {0}",
                        ((DefaultMemberAttribute)attribute).MemberName);
                if (attribute is ConditionalAttribute)
                    Console.WriteLine(" ConditionalString= {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();
        }
    }

 编译并运行上述应用程序,将获得以下输出:

 

两个attribute实例的相互匹配

现在,我们的代码能判断是否将一个attribute的实例应用于一个目标,除此之外,可能还要检查attribute的字段来确定他们的值,为此,一个办法是老老实实的写代码来检查attribute类的字段的值,然而,System.Attribute重写了Object的Equels方法,这个方法内部会比较两个attribute对象中的字段值(为每个字段都调用Equals)。如果所有字段都匹配,就返回true,否则返回false。可以在自己的定制attribute类中重写Equals来移除反射的使用,从而提升性能。

System.Attribute还公开了一个虚方法Match,可重写它来提供更丰富的语义。Match的默认实现只是调用Equal方法并返回它的结果,下例演示了如何重写Equals和Match,后者在一个attribute代表另一个attribute的子集的前提下返回true。另外,还演示了如何使用Match。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

posted on 2013-12-17 00:58  菜菜小Z  阅读(349)  评论(0编辑  收藏  举报