美文网首页
特性(Attribute)

特性(Attribute)

作者: 地坛公园 | 来源:发表于2018-07-15 19:30 被阅读0次

    前言:
    本来打算将特性(Attribute)和反射(Reflection)写在一章里,但感觉反射(Reflection)的内容稍微有点点长,所以新起一章,从学习顺序方面,也是先学反射(Reflection)后学特性(Attribute)。

    (官方文档:https://docs.microsoft.com/zh-cn/dotnet/api/system.type?view=netframework-4.7.2

    概念:
    一、什么是特性(Attribute)?

    CLR(common language runtime)允许你向程序的元素(programming elements)

    (types(classes),结构structs,字段fields,方法methods,属性properties,委托delegates,事件Events,方法参数Params,方法返回值ReturnValue,泛型generic<xxx>参数)

    添加一些描述性的信息,这些特性数据会和元数据(metadata)存放在一起,并通过反射(Reflection)技术运行时获取。

    添加这些Attribute属性的目的是实现一些丰富的功能,比如在设计和调试阶段使用Attribute特性来辅助开发。

    我们在目标元素上应用特性Attribute,会改变目标的行为。比如FlagsAttribute,限定用于System.Enum枚举类型,当在Enum上应用FlagsAttribute后,在运行时,会通过反射Reflection检测Enum是否使用了FlagsAttribute,如果使用,则会改变Enum的ToString和Format的行为.如果没有,就视为一个普通的枚举类型。

    注:元数据(metadata)是用表来存储的,程序集中会定义多个表,如类型定义表(types table),字段定义表(fields table),方法定义表(methods table),属性定义表(properties table)等等

    .net framework出于各种原因,可以通过特性解决许多的问题。
    比如如何序列化和反序列化一个类或是字段[Serializable][NonSerialized],
    提示用户某些方法已经或是将要被废弃[Obsolete],
    某些方法需要通过非拖管的代码来实现[DllImport],
    或是提供一些便利的操作...

    二、常用的特性有哪些?

    [Serializable]//序列化和反序列化,可以添加到类Class,结构Struct,枚举Enum,委托Delegate上,该特性不具有继承性

    [NonSerialized]//声明某个字段Field不需要进行序列化处理,也不具有继承性,这通常是某些需要动态进行计算的值,比如说角色的总战力

    [DllImport]//声明该方法使用非拖管代码来实现,比如Unity3D开发当中,与iOS进行交互,调用iOS的API时,就需要使用该特性

    [Obsolete]//声明某个程序元素(program elements)将不会再使用了,准备废弃了,可以使用在某何程序元素上.

    [Flags]//声明枚举被当做一个位字段来处理(bit field),具体FlagsAttribute的解释请看前面的文章”FlagsAttribute是什么?"

    [AttributeUsage(AttributeTargets.Enum,Inherited=false)]//限制特定只能用于枚举Enum上,并且不具有继承性

    [ParamArrayAttribute]//不定参数实际是一种特性Attribute,void test(params string[] val){...}

    以上都是CLR预定义的特性,可以根据需求自己实现"自定义"CustomAttribute特性.

    三、特性Attribute的使用

    特性Attribute实际上是一个类的实例,必须直接或间接继承自System.Attribute抽象类,CLR预定义的特性Attribute使用时,大多数特性都需要引入命名空间:

    using  System.Runtime.InteropServices;
    

    特性可以定义在任意的命名空间中.

    将特性Attribute应用于目标元素时,相当于调用了该特性Attribute类的实例构造函数,下面是一些在目标元素应用特性Attribute的例子:

    [Serializable]//目标Person为可序列化和反序列化的类
        public class Person{
    //...........
    }
    
    
    [NonSerialized]//字段name 不进行序列化和反序列化
    public string name;//Field
    
    [OptionalField]//指定name字段为可选序列化,这通常用于可序列化的类中,新增成员时,要指定新增的成员是禁止序列化还是可选序列化,如果不添加,那么旧的序列化数据在进行反序列化时,会抛出异常,name无法被反序列化
    
    public string name;//Field
    
    [Flags]//在枚举Vegetables上应用Flags属性,改变枚举的ToString和Format方法的行为
        public enum Vegetables
        {
            Cabbage = 1<<0,
            Carrot = 1<<1,
            Cuke = 1<<2,
            Potato = 1<<3,
        }
    
    [DllImport("__Internal")]//在StartPayment方法上应用DllImport,该方法由非拖管代码实现,如Unity C#与iOS交互
    
    public static extern void StartPayment(string paymentId,int bonusType);
    
    

    在使用特性Attribute时,我们可以添加一个前缀来指定要应用于的目标元素,许多时候,即使是省略前缀,编译器也能判断特性要应用于什么目标元素,如:

    [type:Serializable]
        public class Person{
    //...
    }
    
    [method:Obsolete]
    public Person(string name,int age,bool married,float deposit)
    {...}
    
    [property:someAttr]
    public string plan { ... }
    
    [return:someAttr]
    private string MarriedState(){...}
            
    public static void SaySomething([param:someAttr] string words)
        
    

    特性Attribute在使用时,可以省略后面的Attribute,减少代码量,提高可读性,比如[Serializable]的全称是[SerializableAttribute]

    [Flags]=[FlagsAttribute],但在使用中,我们不需要加上Attribute后缀,在自定义特性的时候,我们需要加上,统一规范。

    前面提到过,特性Attribute实际上是类的实例,我们在使用时,传递的参数,是调用的该特性公共的构造函数,在特性中Attribute中有两种参数:

    1.定位参数(positional parameter),又叫必要参数,即我们必须要传递的参数,和调用常规的函数没有区别,按顺序类型传递需要的参数即可,如:

    [DllImport("__internal")] //DllImportAttribute特性类提供了一个接受string参数的构造函数,传递了字符串__internal就是定位参数
    

    2.命名参数(named parameter),又称为可选参数,他不是必需要设置的,通过命名参数来为特性Attribute类中的公共字段或属性进行赋值,如:

    [DllImport("__internal"),CharSet=CharSet.Auto,SetLastError=true]
    //CharSet和SetLastError是公共的实例字段或属性,通过命名参数来赋值
    

    其它的,可以在目标元素上同时应用多个特性,他们可分别在单独的[](square bracket)中,也可放在一个[](square bracket)中,并以逗号,(comma)分离,并且和顺序无关,以下几个都是等价的(只是演示不同的声明形式,不考虑可行性):

    [Obsolete][Serializable][Flags]
    [Obsolete,Flags,Serializable]
    [FlagsAttribute()][ObsoleteAttribute()][SerializableAttribute()]
    [FlagsAttribute][ObsoleteAttribute][SerializableAttribute]
    

    四、定义自己的特性Attribute类

    首先,定义自己的特性Attribute类,必须要直接或间接的从System.Attribute抽象类派生,并且至少要包含一个公共的构造函数,虽然特性类型是一个类,但这个类非常的简单,除了提供公共的字段和属性,他不应该提供更多的公共方法,事件或其它的成员。通常建议使用属性,而不是字段,便于修改。

    以FlagsAttribute特性为例:

    namespace System.test{
        public class FlagsAttribute:System.Attribute{//派生自抽像类System.Attribute
            //至少提供一个公共的构造函数
            public FlagsAttribute()
            {
            }
        }
    }
    

    有了特性Attribute类以后,我们通常要限定特性的使用范围,即可以在哪些目标元素上使用,这就需要使用预定义的特性System.AttributeUsageAttribute。
    这样我们限制System.test.FlagsAttribute只能应用于枚举Enum:

    namespace System.test{
    
        [AttributeUsage(AttributeTargets.Enum,Inherited=false)]
        public class FlagsAttribute:System.Attribute{//派生自抽像类System.Attribute
            //至少提供一个公共的构造函数
            public FlagsAttribute()
            {
            }
        }
    }
    

    当你在types,field,method,properties,events,delegates,returnvalue,params...上使用时,会出现编译错误。

    AttributeUsage特性类很简单,他提供了三个公共属性,分别是ValidOn,Inherited和AllowMultiple,ValidOn只能get,通过公共的构造函数来set设置ValidOn,一个位标志AttributeTargets.

    ValidOn:应用在哪些目标元素上,可以通过位|(OR)组合,应用于多个目标元素。

    AttributeTargets在FCL中的定义请查看官方文档:
    https://docs.microsoft.com/en-us/dotnet/api/system.attributetargets?view=netframework-4.7.2

    Inherited:是否具有继承性,比如你应用在类上,如果Inherited=false,那么基类使用了该特性,派生类是不会继承该特性的,比如说[Serializable]

    [AttributeUsage(AttributeTargets.Class|AttributeTargets.Method,Inherited=true)]
        public class StrongAttribute:System.Attribute{
            public StrongAttribute()
            {}
        }
            
        [Strong,Serializable]
        public class BaseClass{
    
            [Strong("Base")]
            public virtual void DoSomething(string v)
            {}
        }
    
        public class DerivedClass:BaseClass
        {
            public override void DoSomething(string v)
            {}
        }
    

    BaseClass应用了[Strong,Serializable]特性,DerivedClass派生自BaseClass,所以DerivedClass也继承[Strong]特性,因为Inherited=true,但Derived并不能够被序列化和反序列化,因为Serializable特性类中Inherited=false

    AllowMultiple:是否允许将特性多次应用于同一个目标,通常来说是没有意义的,比如[Flags][Flags],重复定义,没有实际作用,但有些特性还是需要的,比如条件特性类.

    [Conditional("DEBUG")][Conditional("SANDBOX")]
    

    只有在定义了DEBUG或SANDBOX符号的前提下,编译器才会在元数据中生成特性信息。这个在设计和调试阶段,那些用于辅助开发的特性非常有帮助,运行中不需要的特性就不要加到元数据了,减少程序集的大小。

    最后,如果没有指定AttributeUsage特性,那么会使用默认值,即可以使用在所有的目标元素,并且Inherited=true

    五、检测定制的特性Attribute
    我自定义了特性Attribute并应用在目标元素上以后,我在运行时,要进行检测是否使用了该特性,并执行逻辑分支。
    比如枚举应用了Flags特性,那么我在调用ToString的时候,就要检测当前的枚举是否应用了Flags特性,如:

    public override string ToString ()
            {
                //检测枚举类型是否应用了FlagsAttribute特性
                if (this.GetType ().IsDefined (typeof(FlagsAttribute), false)) {
                    //接位标志,以字符串的形式输出
                } else {
                    //当成一个普通的枚举值处理
                }
            }
    

    通过调用Type的IsDefined方法,返回true,表明该目标元素应用了特性。isDefined的第二个参数是Inherited,是否检测特性的派生类,如果你只想检测指定的类,那么额外的检查是没有必要的,可以将特性类设置为sealed密封类。

    如果只是想检测是否应用了某个特性,使用IsDefined就可以了,很高效,不生成实例,但特性中我们可能会传递一些参数,想要获取这些特性的参数,就需要使用另外两个静态方法:

    1.GetCustomAttributes()//返回目标元素上应用的所有特性,通常应用于将AllowMultiple设置为true的特性,一个特性多次用于同一个目标元素上。

    2.GetCustomAttribute()//返回目标元素上应用的指定的特性。
    Retrieves a custom attribute of a specified type applied to an assembly, module, type member, or method parameter.

    可以用于参数parameter和module以及assembly.
    (文档地址:https://docs.microsoft.com/en-us/dotnet/api/system.attribute.getcustomattribute?view=netframework-4.7.2)

    获取特性的实例这些操作是比较消耗的,从性能角度,可以缓存这些方法的返回结果。

    下面实现一个小例子,现应用GetCustomAttributes和GetCustomAttribute方法。(来自于官方DEMO)

    (官方文档:https://docs.microsoft.com/en-us/dotnet/api/system.attribute.getcustomattribute?view=netframework-4.7.2#System_Attribute_GetCustomAttribute_System_Reflection_Module_System_Type_System_Boolean_)

    // Define a custom parameter attribute that takes a single message argument.
    [AttributeUsage( AttributeTargets.Parameter )]
    public class ArgumentUsageAttribute : Attribute
    {
        // This is the attribute constructor.
        public ArgumentUsageAttribute( string UsageMsg )
        {
            this.usageMsg = UsageMsg;
        }
    
        // usageMsg is storage for the attribute message.
        protected string usageMsg;
    
        // This is the Message property for the attribute.
        public string Message
        {
            get { return usageMsg; }
            set { usageMsg = value; }
        }
    }
    
     public class BaseClass 
        {
            // Assign an ArgumentUsage attribute to the strArray parameter.
            // Assign a ParamArray attribute to strList using the params keyword.
            public virtual void TestMethod(
                [ArgumentUsage("Must pass an array here.")]
                String[] strArray,
                params String[] strList)
            { }
        }
    
        public class DerivedClass : BaseClass
        {
            // Assign an ArgumentUsage attribute to the strList parameter.
            // Assign a ParamArray attribute to strList using the params keyword.
            public override void TestMethod(
                String[] strArray,
                [ArgumentUsage("Can pass a parameter list or array here.")]
                params String[] strList)
            { }
        }
    
    public void test()
            {
                Type t = typeof(DerivedClass);
                MethodInfo info = t.GetMethod ("TestMethod");
                ParameterInfo[] pInfoArray = info.GetParameters();
                foreach (var p in pInfoArray) {
                    if (Attribute.IsDefined (p, typeof(ArgumentUsageAttribute))) {
                        ArgumentUsageAttribute usageAttr = (ArgumentUsageAttribute)
                            Attribute.GetCustomAttribute( 
                                p, typeof(ArgumentUsageAttribute) );
    
                        if (usageAttr != null) {
                            Debug.Log ("Usage:"+usageAttr.Message);
                            if (!p.ParameterType.IsArray) {
                                Debug.LogError ("You must set parameter to Array!");
                            }
                        }
                    }
                }
            }
    
    
    

    创建了一个特性Attribute类ArgumentUsage,默认可继承
    又创建了一个BaseClass和DerivedClass派生类,
    在基类的virtual方法中第一个参数加上特性 [ArgumentUsage("Must pass an array here.")],
    因为ArgumentUsage具有继承性,所以派生类DerivedClass中重载的方法的参数,
    也继承[ArgumentUsage("Must pass an array here.")],在派生类DerivedClass中又加入了 [ArgumentUsage("Can pass a parameter list or array here.")]特性,
    那么在test2方法中,我通过反射Reflection来获取方法TestMethod,并获取参数是否应用ArgumentUsage特性。

    答案是Must pass an array here和Can pass a parameter list or array here都会输出。

    下面加了一条判断if (!p.ParameterType.IsArray),判断参数如果不是数组,则抛出异常。

    (GetCustomAttributes的例子就不列了,返回目标元素上的使用的特性数组。)

    注:这里实测时在unity上有问题,在VS IDE上测试是正常通过,在unity下测试Derived中的string[] strArray并没有继承自父类BaseClass的参数的特性,初步断定是framework的问题,毕竟u3d目前使用的mono版本还比较老,还存在一些问题,比如foreach,这个问题有知道的同学麻烦指点一下,稍后会将该问题抛到stackoverflow上。
    (stackoverflow问题地址:https://stackoverflow.com/questions/51351402/c-sharp-attribute-could-not-inherited-in-parameter-of-method)


    到此为止,如果大家发现有什么不对的地方,欢迎指正,共同提高,感谢您的阅读!

    编辑于2018.7.15

    --闲言碎语

    今天是2018年的7月15日,世界杯决赛的日子,早上曼尼帕奎奥在第七回合TKO了对手,酣畅淋漓,晚上的世界杯决赛我支持克罗地亚,世界杯让我对莫德里奇有了新的认识,开始我是支持Brazil,支持保利尼奥,支持库蒂尼奥,支持马塞洛,可惜巴西在对阵比利时的比赛中,发挥得并不好,裁判也有争议性的判罚,很遗憾,法国一直踢得比较顺利,所以希望今天克罗地亚可以给法国多制造一些麻烦,我希望莫德里奇拿到金球奖!

    61add42aly1ft8ga53g29j20i20muabh.jpg

    相关文章

      网友评论

          本文标题:特性(Attribute)

          本文链接:https://www.haomeiwen.com/subject/syckpftx.html