特性(Attribute)

前言:
本来打算将特性(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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,616评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,020评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,078评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,040评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,154评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,265评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,298评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,072评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,491评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,795评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,970评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,654评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,272评论 3 318
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,985评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,223评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,815评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,852评论 2 351

推荐阅读更多精彩内容