設計類型的時候可以使用各種成員來描述該類型的信息,但有時候我們可能不太願意將一些附加信息放到類的內部,因為這樣,可能會給類型本身的信息描述帶來麻煩或誤解。我們想為類型、屬性、方法及返回值附加額外的信息,這些附加信息可以更明確的表達類及其對象成員的狀態,怎么辦?定制特性Attribute可以做到。
為了避免Attribute與Property翻譯性誤解,我們以下的討論中將以特性表示Attribute。
細心的讀者可能會發現如下類似定義:
//項目的AssemblyInfo.cs文件內有: [assembly: Guid("df510f85-e549-4999-864d-bb892545690b")] [assembly: AssemblyVersion("1.0.0.0")] //也可能會發現有些類前面也有類似的語句: [Serializable, ComVisible(true)] public sealed class String {} //在我們開發WCF項目時,定義接口契約時接口前面也有類似的語句: [ServiceContract] public interface IService {}
這些放在方括弧[]中的Serializable、ServiceContract就是.NET Framework提供的特性Attribute。它們有的作用於程序集,有的作用於類和接口,也有的作用於屬性和方法等其他成員。
特性Attribute是指給聲明性對象附加一些聲明性描述信息,這些信息在經過編譯器編譯后相當於目標對象的自描述信息被編譯進托管模塊的元數據中,很顯然,如果這些描述信息太多,會大大增加元數據的體積,這一點要注意。編譯器只是將這些描述信息編譯生成到元數據中,而對Attribute的“邏輯”並不關注。
前面提到的AssemblyVersion 、Serializable、ServiceContrac等都是繼承於System.Attribute類,CLS要求定制Attribute必須繼承於System.Attribute類,為了符合規范,所有的定制特性都要以Attribute后綴,這只是一個規范,也可以不使用此后綴,並沒有強制。即使采用了后綴,為了方便編碼,C#編譯器也是允許在編碼時省略后綴的,而VS智能提示對此也有很好的支持。
如下我們定義了一個有關國家的定制特性:
public class CountryAttribute : Attribute { public CountryAttribute(string name) { this.Name = name; } public int PlayerCount { get; set; } public string Name { get; set; } }
來看一下編譯干了什么:
可以看到,定制特性就是一個普通的類,繼承了System.Attribute類,沒有什么特殊的地方。非抽象的定制特性類必須有至少一個公共構造函數,因為在將一個定制特性應用於其他目標元素時是以定制特性的實例起作用的。定制特性應該注意以下幾點:
(1) 可以在定制特性內提供公共字段和屬性,但不應該提供任何公共方法、事件等成員,像上面代碼中的屬性Name和PlayerCount都是被允許的。
(2) 公共實例構造函數可以有參數,也可以無參數,也可以同時提供多個構造函數。如上面的CountryAttribute類可以增加一個無參的構造函數:
public CountryAttribute() { }
(3)定義Attribute類的實例構造函數的參數、字段和屬性時,只能使用以下數據類型:object,type,string,Boolean,char,byte,sbyte,Int16,int,UInt16,UInt32,Int64,UInt64,Single,double,枚舉和一維0基數組。
前面的定制特性CountryAttribute可以應用於任何目標元素?如果我們希望它只應用於類類型或方法時怎么辦呢?.NET Framework當然提供了這一方面的支持:System. AttributeUsageAttribute類。AttributeUsageAttribute是.NET Framework提供的一個定制特性,它主要是作用於其他定制特性來限制目標定制特性的作用目標。看一下其定義:
public sealed class AttributeUsageAttribute : Attribute { public AttributeUsageAttribute(AttributeTargets validOn); public bool AllowMultiple { get; set; } public bool Inherited { get; set; } public AttributeTargets ValidOn { get; } }
(1)該類只提供了一個公共實例構造器,其接收的參數validOn是枚舉類型AttributeTargets,它指定了定制特性的作用范圍,比如:程序集、模塊、結構、類等。
public enum AttributeTargets { Assembly = 1, Module = 2, Class = 4, Struct = 8, Enum = 16, Constructor = 32, Method = 64, Property = 128, Field = 256, Event = 512, Interface = 1024, Parameter = 2048, Delegate = 4096, ReturnValue = 8192, GenericParameter = 16384, All = 32767, }
(2)AttributeUsageAttribute有一個附加屬性AllowMultiple,它表示是否允許將定制特性的實例多次應用於同一個目標元素。
(3)AttributeUsageAttribute還有一個附加屬性Inherited,它表示定制特性應用於基類時,是否將該特性同時應用於派生類及重寫的的成員。
我們對CountryAttribute類進行了改造,同時定義了兩個類使用定制特性CountryAttribute,如下代碼:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.ReturnValue | AttributeTargets.Property, AllowMultiple = true, Inherited = true)] public class CountryAttribute : Attribute { public CountryAttribute() { } public CountryAttribute(string name) { this.Name = name; } public int PlayerCount { get; set; } public string Name { get; set; } } [Country("China")] [Country("America")] public class Sportsman { public string Name { get; set; } [Country(PlayerCount = 5)] public virtual void Play() { } } public class Hoopster : Sportsman { public override void Play() { } }
我們將CountryAttibute特性限定只能用於類、方法、方法返回值和屬性:
AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.ReturnValue | AttributeTargets.Property
並且允許該定制特性的實例多次應用於同一個目標元素:
AllowMultiple = true
同時還要求將該特性不僅用於基類,也應用於派生類及重寫成員:
Inherited = true
在Sportsman類我們應用了兩次定制特性 [Country("China")]和[Country("America")]。對基類Sportsman及其成員方法Play()我們使用了定制特性,在其派生類Hoopster同樣可以得到這些定制特性,下面討論中我們將驗證這一點。
細心的你可能會發現[Country("China")]和[Country(PlayerCount = 5)]有點相似但又有些不同,為什么?
(1) 我們在定義定制特性的時候是CountryAttribute,這里可以簡寫為Country。在將Country應用於某目標元素時,編譯器進行編譯時已經確認它是一個定制特性,接着它會在Attribute繼承類中查找Country,如果沒找到,則會加上Attribute后綴繼續查找,再找不到,就會“啪”的一聲報錯了!
(2) Country("China")是在為實例構造器傳遞參數”China”,這沒什么好解釋的,問題是[Country(PlayerCount = 5)],我們並沒有為County的構造函數設置參數PlayerCount啊。先來看一下在VS中編寫代碼時的智能提示:
這種特殊的語法將定制特性的字段和屬性認定為命名參數,它允許定制特性對象構造完了之后,使用命名參數設置對象的公共字段和屬性。這就提供了很大的靈活性,你可以將實例構造器的參數設為公共的字段或屬性,也可以將公共的字段和屬性設為私有,然后在實例構造函數處接收參數再設置它們。當然,有一點,如果使用實例構造函數,則該函數的參數都必須提供值,如果使用公共字段和屬性(命名參數),則可以部分提供值,其他字段和屬性可以維持默認值。建議使用屬性而不是字段,可以對其進行更多的控制。
最后我們再來看一下編譯器對使用了定制特性的類是干了什么?
定制特性的America和China是被寫入到元數據中的。
如果僅僅是對目標元素應用了定制特性,好像意義並不大,更重要的是在應用了特性之后,我們要使用這些特性附帶的信息。通常是通過反射(Reflection)配合System.Attribute的靜態方法來使用特性信息。先來看一下System.Attribute的三個重要的靜態方法:
IsDefined 判斷指定的目標元素是否應用了System.Attribute的派生類(定制特性),它有多個重載。
GetCustomAttribute 返回應用於目標元素的與指定類型一致的特性對象,如果目標元素沒有應用特性實例則返回null;如果目標元素應用了指定特性類型的多個實例,則拋出異常,它也有多個重載。
GetCustomAttributes 返回應用於目標元素的特性數組,在其重載方法中,也可以指定特性類型,它也有多個重載。
我們新定義一個定制特性:
[AttributeUsage(AttributeTargets.All)] public class TestAttribute : Attribute { }
AttributeTargets.All指出可以將該特性應用於任何目標元素。
改造一下Sportsman類:
[Country("China")] [Country("America")] public class Sportsman { public string Name { get; set; } [Country("Sports")] public virtual void Play() { Console.WriteLine("Play"); } }
對方法Play()應用了[Country("Sports")],表明了運動類型為體育運動Sports。接着我們改造Hoopster類的Play()方法:
public class Hoopster : Sportsman { public override void Play() { MemberInfo[] members = this.GetType().GetMembers(); foreach (MemberInfo member in members) { Attribute testAttr = Attribute.GetCustomAttribute(member, typeof(TestAttribute)); if (testAttr != null && testAttr is TestAttribute) { Console.WriteLine(((TestAttribute)testAttr).Message); } if (Attribute.IsDefined(member, typeof(CountryAttribute))) { Attribute[] attributes = Attribute.GetCustomAttributes(member); foreach (Attribute item in attributes) { CountryAttribute attr = item as CountryAttribute; if (attr != null) { Console.WriteLine(string.Format("運動類型:{0} 運動員人數:{1}", attr.Name, attr.PlayerCount)); } } } } } }
獲取當前對象的全部成員后,接着循環每一個成員。
無論是Sportsman類還是Hoopster類都沒有應用TestAttribute特性,所以testAttr將一直保持為null。
接着我們應用Attribute.IsDefined方法判斷每一個成員是否應用了定制特性CountryAttribute。如果應用了,接着獲取所有應用於該成員的特性。然后循環特性,如果特性是定制特性CountryAttribute類型,則打印出我們定義的運動類型和運動員人數。運行結果如下:
很明顯,我們對基類Sportsman方法Play的定義[Country("Sports")]已經影響到了子類Hoopster,這驗證了我們前面所說的Inherited = true的作用。由於我們未給PlayerCount賦值,所以它依然是默認值0。
接下來我們繼續改造子類Hoopster的方法Play:
[Country("Ball", PlayerCount = 5)] public override void Play() { //... }
再來看看它的運行結果:
這一次不僅打印出了Sports/0,還打印出了Ball/5,這是因為我們為子類Hoopster的Play方法應用了[Country("Ball", PlayerCount = 5)]特性,方法Play不僅得到了基類的特性信息,也擁有自己的特性信息。