摘 要
設計類型的時候可以使用各種成員來描述該類型的資訊,但有時候我們可能不太願意將一些附加資訊放到類的內部,因為這樣,可能會給類型本身的資訊描述帶來麻煩或誤解。我們想為類型、屬性、方法及傳回值附加額外的資訊,這些附加資訊可以更明確的表達類及其對象成員的狀態,怎麼辦?定製特性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是指給聲明性對象附加一些聲明性描述資訊,這些資訊在經過編譯器編譯後相當於目標對象的自描述資訊被編譯進託管模組的中繼資料中,很顯然,如果這些描述資訊太多,會大大增加中繼資料的體積,這一點要注意。編譯器只是將這些描述資訊編譯產生到中繼資料中,而對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是被寫入到中繼資料中的。
第二節 應用Attribute
如果僅僅是對目標元素應用了定製特性,好像意義並不大,更重要的是在應用了特性之後,我們要使用這些特性附帶的資訊。通常是通過反射(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不僅得到了基類的特性資訊,也擁有自己的特性資訊。
小 結