前言:
本來打算將特性(Attribute)和反射(Reflection)寫在一章里,但感覺反射(Reflection)的內(nèi)容稍微有點(diǎn)點(diǎn)長敬察,所以新起一章秀睛,從學(xué)習(xí)順序方面,也是先學(xué)反射(Reflection)后學(xué)特性(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),結(jié)構(gòu)structs,字段fields,方法methods,屬性properties,委托delegates,事件Events,方法參數(shù)Params,方法返回值ReturnValue,泛型generic<xxx>參數(shù))
添加一些描述性的信息虫给,這些特性數(shù)據(jù)會(huì)和元數(shù)據(jù)(metadata)存放在一起藤抡,并通過反射(Reflection)技術(shù)運(yùn)行時(shí)獲取。
添加這些Attribute屬性的目的是實(shí)現(xiàn)一些豐富的功能抹估,比如在設(shè)計(jì)和調(diào)試階段使用Attribute特性來輔助開發(fā)缠黍。
我們?cè)谀繕?biāo)元素上應(yīng)用特性Attribute,會(huì)改變目標(biāo)的行為药蜻。比如FlagsAttribute瓷式,限定用于System.Enum枚舉類型,當(dāng)在Enum上應(yīng)用FlagsAttribute后语泽,在運(yùn)行時(shí)贸典,會(huì)通過反射Reflection檢測Enum是否使用了FlagsAttribute,如果使用踱卵,則會(huì)改變Enum的ToString和Format的行為.如果沒有廊驼,就視為一個(gè)普通的枚舉類型据过。
注:元數(shù)據(jù)(metadata)是用表來存儲(chǔ)的,程序集中會(huì)定義多個(gè)表妒挎,如類型定義表(types table),字段定義表(fields table)绳锅,方法定義表(methods table),屬性定義表(properties table)等等
.net framework出于各種原因酝掩,可以通過特性解決許多的問題鳞芙。
比如如何序列化和反序列化一個(gè)類或是字段[Serializable][NonSerialized],
提示用戶某些方法已經(jīng)或是將要被廢棄[Obsolete]期虾,
某些方法需要通過非拖管的代碼來實(shí)現(xiàn)[DllImport]原朝,
或是提供一些便利的操作...
二、常用的特性有哪些镶苞?
[Serializable]//序列化和反序列化喳坠,可以添加到類Class,結(jié)構(gòu)Struct,枚舉Enum,委托Delegate上宾尚,該特性不具有繼承性
[NonSerialized]//聲明某個(gè)字段Field不需要進(jìn)行序列化處理丙笋,也不具有繼承性,這通常是某些需要?jiǎng)討B(tài)進(jìn)行計(jì)算的值煌贴,比如說角色的總戰(zhàn)力
[DllImport]//聲明該方法使用非拖管代碼來實(shí)現(xiàn)御板,比如Unity3D開發(fā)當(dāng)中,與iOS進(jìn)行交互牛郑,調(diào)用iOS的API時(shí)怠肋,就需要使用該特性
[Obsolete]//聲明某個(gè)程序元素(program elements)將不會(huì)再使用了,準(zhǔn)備廢棄了,可以使用在某何程序元素上.
[Flags]//聲明枚舉被當(dāng)做一個(gè)位字段來處理(bit field),具體FlagsAttribute的解釋請(qǐng)看前面的文章”FlagsAttribute是什么?"
[AttributeUsage(AttributeTargets.Enum,Inherited=false)]//限制特定只能用于枚舉Enum上淹朋,并且不具有繼承性
[ParamArrayAttribute]//不定參數(shù)實(shí)際是一種特性Attribute,void test(params string[] val){...}
以上都是CLR預(yù)定義的特性笙各,可以根據(jù)需求自己實(shí)現(xiàn)"自定義"CustomAttribute特性.
三、特性Attribute的使用
特性Attribute實(shí)際上是一個(gè)類的實(shí)例础芍,必須直接或間接繼承自System.Attribute抽象類杈抢,CLR預(yù)定義的特性Attribute使用時(shí),大多數(shù)特性都需要引入命名空間:
using System.Runtime.InteropServices;
特性可以定義在任意的命名空間中.
將特性Attribute應(yīng)用于目標(biāo)元素時(shí)仑性,相當(dāng)于調(diào)用了該特性Attribute類的實(shí)例構(gòu)造函數(shù)惶楼,下面是一些在目標(biāo)元素應(yīng)用特性Attribute的例子:
[Serializable]//目標(biāo)Person為可序列化和反序列化的類
public class Person{
//...........
}
[NonSerialized]//字段name 不進(jìn)行序列化和反序列化
public string name;//Field
[OptionalField]//指定name字段為可選序列化,這通常用于可序列化的類中诊杆,新增成員時(shí)歼捐,要指定新增的成員是禁止序列化還是可選序列化,如果不添加晨汹,那么舊的序列化數(shù)據(jù)在進(jìn)行反序列化時(shí)豹储,會(huì)拋出異常,name無法被反序列化
public string name;//Field
[Flags]//在枚舉Vegetables上應(yīng)用Flags屬性淘这,改變枚舉的ToString和Format方法的行為
public enum Vegetables
{
Cabbage = 1<<0,
Carrot = 1<<1,
Cuke = 1<<2,
Potato = 1<<3,
}
[DllImport("__Internal")]//在StartPayment方法上應(yīng)用DllImport剥扣,該方法由非拖管代碼實(shí)現(xiàn)巩剖,如Unity C#與iOS交互
public static extern void StartPayment(string paymentId,int bonusType);
在使用特性Attribute時(shí),我們可以添加一個(gè)前綴來指定要應(yīng)用于的目標(biāo)元素钠怯,許多時(shí)候球及,即使是省略前綴,編譯器也能判斷特性要應(yīng)用于什么目標(biāo)元素呻疹,如:
[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在使用時(shí),可以省略后面的Attribute筹陵,減少代碼量刽锤,提高可讀性,比如[Serializable]的全稱是[SerializableAttribute]
[Flags]=[FlagsAttribute]朦佩,但在使用中并思,我們不需要加上Attribute后綴,在自定義特性的時(shí)候语稠,我們需要加上宋彼,統(tǒng)一規(guī)范。
前面提到過仙畦,特性Attribute實(shí)際上是類的實(shí)例输涕,我們?cè)谑褂脮r(shí),傳遞的參數(shù)慨畸,是調(diào)用的該特性公共的構(gòu)造函數(shù)莱坎,在特性中Attribute中有兩種參數(shù):
1.定位參數(shù)(positional parameter),又叫必要參數(shù),即我們必須要傳遞的參數(shù)寸士,和調(diào)用常規(guī)的函數(shù)沒有區(qū)別檐什,按順序類型傳遞需要的參數(shù)即可,如:
[DllImport("__internal")] //DllImportAttribute特性類提供了一個(gè)接受string參數(shù)的構(gòu)造函數(shù)弱卡,傳遞了字符串__internal就是定位參數(shù)
2.命名參數(shù)(named parameter),又稱為可選參數(shù)乃正,他不是必需要設(shè)置的,通過命名參數(shù)來為特性Attribute類中的公共字段或?qū)傩赃M(jìn)行賦值婶博,如:
[DllImport("__internal"),CharSet=CharSet.Auto,SetLastError=true]
//CharSet和SetLastError是公共的實(shí)例字段或?qū)傩晕途撸ㄟ^命名參數(shù)來賦值
其它的,可以在目標(biāo)元素上同時(shí)應(yīng)用多個(gè)特性凡蜻,他們可分別在單獨(dú)的[](square bracket)中搭综,也可放在一個(gè)[](square bracket)中,并以逗號(hào)划栓,(comma)分離兑巾,并且和順序無關(guān),以下幾個(gè)都是等價(jià)的(只是演示不同的聲明形式忠荞,不考慮可行性):
[Obsolete][Serializable][Flags]
[Obsolete,Flags,Serializable]
[FlagsAttribute()][ObsoleteAttribute()][SerializableAttribute()]
[FlagsAttribute][ObsoleteAttribute][SerializableAttribute]
四蒋歌、定義自己的特性Attribute類
首先帅掘,定義自己的特性Attribute類,必須要直接或間接的從System.Attribute抽象類派生堂油,并且至少要包含一個(gè)公共的構(gòu)造函數(shù)修档,雖然特性類型是一個(gè)類,但這個(gè)類非常的簡單府框,除了提供公共的字段和屬性吱窝,他不應(yīng)該提供更多的公共方法,事件或其它的成員迫靖。通常建議使用屬性,而不是字段院峡,便于修改。
以FlagsAttribute特性為例:
namespace System.test{
public class FlagsAttribute:System.Attribute{//派生自抽像類System.Attribute
//至少提供一個(gè)公共的構(gòu)造函數(shù)
public FlagsAttribute()
{
}
}
}
有了特性Attribute類以后系宜,我們通常要限定特性的使用范圍照激,即可以在哪些目標(biāo)元素上使用,這就需要使用預(yù)定義的特性System.AttributeUsageAttribute盹牧。
這樣我們限制System.test.FlagsAttribute只能應(yīng)用于枚舉Enum:
namespace System.test{
[AttributeUsage(AttributeTargets.Enum,Inherited=false)]
public class FlagsAttribute:System.Attribute{//派生自抽像類System.Attribute
//至少提供一個(gè)公共的構(gòu)造函數(shù)
public FlagsAttribute()
{
}
}
}
當(dāng)你在types,field,method,properties,events,delegates,returnvalue,params...上使用時(shí)俩垃,會(huì)出現(xiàn)編譯錯(cuò)誤。
AttributeUsage特性類很簡單汰寓,他提供了三個(gè)公共屬性口柳,分別是ValidOn,Inherited和AllowMultiple,ValidOn只能get,通過公共的構(gòu)造函數(shù)來set設(shè)置ValidOn,一個(gè)位標(biāo)志AttributeTargets.
ValidOn:應(yīng)用在哪些目標(biāo)元素上,可以通過位|(OR)組合有滑,應(yīng)用于多個(gè)目標(biāo)元素啄清。
AttributeTargets在FCL中的定義請(qǐng)查看官方文檔:
https://docs.microsoft.com/en-us/dotnet/api/system.attributetargets?view=netframework-4.7.2
Inherited:是否具有繼承性,比如你應(yīng)用在類上俺孙,如果Inherited=false辣卒,那么基類使用了該特性,派生類是不會(huì)繼承該特性的睛榄,比如說[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應(yīng)用了[Strong,Serializable]特性荣茫,DerivedClass派生自BaseClass,所以DerivedClass也繼承[Strong]特性,因?yàn)镮nherited=true场靴,但Derived并不能夠被序列化和反序列化啡莉,因?yàn)镾erializable特性類中Inherited=false
AllowMultiple:是否允許將特性多次應(yīng)用于同一個(gè)目標(biāo),通常來說是沒有意義的旨剥,比如[Flags][Flags]咧欣,重復(fù)定義,沒有實(shí)際作用轨帜,但有些特性還是需要的魄咕,比如條件特性類.
[Conditional("DEBUG")][Conditional("SANDBOX")]
只有在定義了DEBUG或SANDBOX符號(hào)的前提下,編譯器才會(huì)在元數(shù)據(jù)中生成特性信息蚌父。這個(gè)在設(shè)計(jì)和調(diào)試階段哮兰,那些用于輔助開發(fā)的特性非常有幫助毛萌,運(yùn)行中不需要的特性就不要加到元數(shù)據(jù)了,減少程序集的大小喝滞。
最后阁将,如果沒有指定AttributeUsage特性,那么會(huì)使用默認(rèn)值右遭,即可以使用在所有的目標(biāo)元素做盅,并且Inherited=true
五、檢測定制的特性Attribute
我自定義了特性Attribute并應(yīng)用在目標(biāo)元素上以后窘哈,我在運(yùn)行時(shí)言蛇,要進(jìn)行檢測是否使用了該特性,并執(zhí)行邏輯分支宵距。
比如枚舉應(yīng)用了Flags特性,那么我在調(diào)用ToString的時(shí)候吨拗,就要檢測當(dāng)前的枚舉是否應(yīng)用了Flags特性满哪,如:
public override string ToString ()
{
//檢測枚舉類型是否應(yīng)用了FlagsAttribute特性
if (this.GetType ().IsDefined (typeof(FlagsAttribute), false)) {
//接位標(biāo)志,以字符串的形式輸出
} else {
//當(dāng)成一個(gè)普通的枚舉值處理
}
}
通過調(diào)用Type的IsDefined方法劝篷,返回true哨鸭,表明該目標(biāo)元素應(yīng)用了特性。isDefined的第二個(gè)參數(shù)是Inherited娇妓,是否檢測特性的派生類像鸡,如果你只想檢測指定的類,那么額外的檢查是沒有必要的哈恰,可以將特性類設(shè)置為sealed密封類只估。
如果只是想檢測是否應(yīng)用了某個(gè)特性,使用IsDefined就可以了着绷,很高效蛔钙,不生成實(shí)例,但特性中我們可能會(huì)傳遞一些參數(shù)荠医,想要獲取這些特性的參數(shù)吁脱,就需要使用另外兩個(gè)靜態(tài)方法:
1.GetCustomAttributes()//返回目標(biāo)元素上應(yīng)用的所有特性,通常應(yīng)用于將AllowMultiple設(shè)置為true的特性彬向,一個(gè)特性多次用于同一個(gè)目標(biāo)元素上兼贡。
2.GetCustomAttribute()//返回目標(biāo)元素上應(yīng)用的指定的特性。
Retrieves a custom attribute of a specified type applied to an assembly, module, type member, or method parameter.
可以用于參數(shù)parameter和module以及assembly.
(文檔地址:https://docs.microsoft.com/en-us/dotnet/api/system.attribute.getcustomattribute?view=netframework-4.7.2)
獲取特性的實(shí)例這些操作是比較消耗的娃胆,從性能角度遍希,可以緩存這些方法的返回結(jié)果。
下面實(shí)現(xiàn)一個(gè)小例子里烦,現(xiàn)應(yīng)用GetCustomAttributes和GetCustomAttribute方法孵班。(來自于官方DEMO)
// 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!");
}
}
}
}
}
創(chuàng)建了一個(gè)特性Attribute類ArgumentUsage涉兽,默認(rèn)可繼承
又創(chuàng)建了一個(gè)BaseClass和DerivedClass派生類,
在基類的virtual方法中第一個(gè)參數(shù)加上特性 [ArgumentUsage("Must pass an array here.")],
因?yàn)锳rgumentUsage具有繼承性篙程,所以派生類DerivedClass中重載的方法的參數(shù)枷畏,
也繼承[ArgumentUsage("Must pass an array here.")],在派生類DerivedClass中又加入了 [ArgumentUsage("Can pass a parameter list or array here.")]特性,
那么在test2方法中虱饿,我通過反射Reflection來獲取方法TestMethod拥诡,并獲取參數(shù)是否應(yīng)用ArgumentUsage特性。
答案是Must pass an array here和Can pass a parameter list or array here都會(huì)輸出氮发。
下面加了一條判斷if (!p.ParameterType.IsArray)渴肉,判斷參數(shù)如果不是數(shù)組,則拋出異常爽冕。
(GetCustomAttributes的例子就不列了仇祭,返回目標(biāo)元素上的使用的特性數(shù)組。)
注:這里實(shí)測時(shí)在unity上有問題颈畸,在VS IDE上測試是正常通過乌奇,在unity下測試Derived中的string[] strArray并沒有繼承自父類BaseClass的參數(shù)的特性,初步斷定是framework的問題眯娱,畢竟u3d目前使用的mono版本還比較老礁苗,還存在一些問題,比如foreach徙缴,這個(gè)問題有知道的同學(xué)麻煩指點(diǎn)一下试伙,稍后會(huì)將該問題拋到stackoverflow上。
(stackoverflow問題地址:https://stackoverflow.com/questions/51351402/c-sharp-attribute-could-not-inherited-in-parameter-of-method)
到此為止,如果大家發(fā)現(xiàn)有什么不對(duì)的地方,歡迎指正,共同提高,感謝您的閱讀!
編輯于2018.7.15
--閑言碎語
今天是2018年的7月15日于样,世界杯決賽的日子疏叨,早上曼尼帕奎奧在第七回合TKO了對(duì)手,酣暢淋漓穿剖,晚上的世界杯決賽我支持克羅地亞考廉,世界杯讓我對(duì)莫德里奇有了新的認(rèn)識(shí),開始我是支持Brazil携御,支持保利尼奧昌粤,支持庫蒂尼奧,支持馬塞洛啄刹,可惜巴西在對(duì)陣比利時(shí)的比賽中涮坐,發(fā)揮得并不好,裁判也有爭議性的判罰誓军,很遺憾袱讹,法國一直踢得比較順利,所以希望今天克羅地亞可以給法國多制造一些麻煩,我希望莫德里奇拿到金球獎(jiǎng)捷雕!