c#特性
想想看如果有一個消息系統(tǒng),它存在這樣一個方法,用來將一則短消息發(fā)送給某人:
- // title: 標(biāo)題;author:作者;content:內(nèi)容;receiverId:接受者Id
 - public bool SendMsg(string title, string author, string content, int receiverId){
 - // Do Send Action
 - }
 
我們很快就發(fā)現(xiàn)這樣將參數(shù)一個個羅列到方法的參數(shù)列表中擴(kuò)展性很糟糕,我們***定義一個Message類將短消息封裝起來,然后給方法傳遞一個Message對象:
- public class Message{
 - private string title;
 - private string author;
 - private string content;
 - private int receiverId;
 - // 略
 - }
 - public bool SendMsg(Messag msg){
 - // Do some Action
 - }
 
此時,我們或許應(yīng)該將舊的方法刪除,用這個擴(kuò)展性更好的SendMsg方法來取代。遺憾的是我們往往不能,因為這組程序可能作為一組API發(fā)布,在很多客戶程序中已經(jīng)在使用舊版本的SendMsg()方法,如果我們在更新程序的時候簡單地刪除掉舊的SendMsg()方法,那么將造成使用老版本SendMsg()方法的客戶程序不能工作。
這個時候,我們該如果做呢?我們當(dāng)然可以通過方法重載來完成,這樣就不用刪除舊的SendMsg()方法了。但是如果新的SendMsg()不僅優(yōu)化了參數(shù)的傳遞,并且在算法和效率上也進(jìn)行了全面的優(yōu)化,那么我們將會迫切希望告知客戶程序現(xiàn)在有一個全新的高性能SendMsg()方法可供使用,但此時客戶程序并不知道已經(jīng)存在一個新的SendMsg方法,我們又該如何做呢?我們可以打電話告訴維護(hù)客戶程序的程序員,或者發(fā)電子郵件給他,但這樣顯然不夠方便,***有一種辦法能讓他一編譯項目,只要存在對舊版本SendMsg()方法的調(diào)用,就會被編譯器告知。
.Net 中可以使用特性來完成這一工作。特性是一個對象,它可以加載到程序集及程序集的對象中,這些對象包括 程序集本身、模塊、類、接口、結(jié)構(gòu)、構(gòu)造函數(shù)、方法、方法參數(shù)等,加載了特性的對象稱作特性的目標(biāo)
特性的英文名稱叫做Attribute,在有的書中,將它翻譯為“屬性”;另一些書中,將它翻譯為“特性”;由于通常我們將含有g(shù)et和/或set訪問器的類成員稱為“屬性”(英文Property),所以本文中我將使用“特性”這個名詞,以區(qū)分“屬性”(Property)。
我們通過這個例子來看一下特性是如何解決上面的問題:我們可以給舊的SendMsg()方法上面加上Obsolete特性來告訴編譯器這個方法已經(jīng)過時,然后當(dāng)編譯器發(fā)現(xiàn)當(dāng)程序中有地方在使用這個用Obsolete標(biāo)記過的方法時,就會給出一個警告信息。
- namespace Attribute {
 - public class Message {}
 - public class TestClass {
 - // 添加Obsolete特性
 - [Obsolete("請使用新的SendMsg(Message msg)重載方法")]
 - public static void ShowMsg() {
 - Console.WriteLine("這是舊的SendMsg()方法");
 - }
 - public static void ShowMsg(Message msg) {
 - Console.WriteLine("新SendMsg()方法");
 - }
 - }
 - class Program {
 - static void Main(string[] args) {
 - TestClass.ShowMsg();
 - TestClass.ShowMsg(new Message());
 - }
 - }
 - }
 
現(xiàn)在運(yùn)行這段代碼,我們會發(fā)現(xiàn)編譯器給出了一個警告:警告CS0618: “Attribute.TestClass.ShowMsg()”已過時:“請使用新的SendMsg(Message msg)重載方法”。通過使用特性,我們可以看到編譯器給出了警告信息,告訴客戶程序存在一個新的方法可供使用,這樣,程序員在看到這個警告信息后,便會考慮使用新的SendMsg()方法。
通過上面的例子,我們已經(jīng)大致看到特性的使用方法:首先是有一對方括號“[]”,在左方括號“[”后緊跟特性的名稱,比如Obsolete,隨后是一個圓括號“()”。和普通的類不同,這個圓括號不光可以寫入構(gòu)造函數(shù)的參數(shù),還可以給類的屬性賦值,在Obsolete的例子中,僅傳遞了構(gòu)造函數(shù)參數(shù)。
使用構(gòu)造函數(shù)參數(shù),參數(shù)的順序必須同構(gòu)造函數(shù)聲明時的順序相同,所有在特性中也叫位置參數(shù)(Positional Parameters),與此相應(yīng),屬性參數(shù)也叫做命名參數(shù)(Named Parameters)。
如果不能自己定義一個特性并使用它,我想你怎么也不能很好的理解特性,我們現(xiàn)在就自己構(gòu)建一個特性。假設(shè)我們有這樣一個很常見的需求:我們在創(chuàng)建或者更新一個類文件時,需要說明這個類是什么時候、由誰創(chuàng)建的,在以后的更新中還要說明在什么時候由誰更新的,可以記錄也可以不記錄更新的內(nèi)容,以往你會怎么做呢?是不是像這樣在類的上面給類添加注釋:
- //更新:jayce, 2016-9-10, 修改 ToString()方法
 - //更新:pop, 2016-9-18
 - //創(chuàng)建:code, 2016-10-1
 - public class DemoClass{
 - // Class Body
 - }
 
這樣的的確確是可以記錄下來,但是如果有一天我們想將這些記錄保存到數(shù)據(jù)庫中作以備份呢?你是不是要一個一個地去查看源文件,找出這些注釋,再一條條插入數(shù)據(jù)庫中呢?
通過上面特性的定義,我們知道特性可以用于給類型添加元數(shù)據(jù)(描述數(shù)據(jù)的數(shù)據(jù),包括數(shù)據(jù)是否被修改、何時創(chuàng)建、創(chuàng)建人,這些數(shù)據(jù)可以是一個類、方法、屬性),這些元數(shù)據(jù)可以用于描述類型。那么在此處,特性應(yīng)該會派上用場。那么在本例中,元數(shù)據(jù)應(yīng)該是:注釋類型(“更新”或者“創(chuàng)建”),修改人,日期,備注信息(可有可無)。而特性的目標(biāo)類型是DemoClass類。
按照對于附加到DemoClass類上的元數(shù)據(jù)的理解,我們先創(chuàng)建一個封裝了元數(shù)據(jù)的類RecordAttribute:
- public class RecordAttribute {
 - private string recordType; // 記錄類型:更新/創(chuàng)建
 - private string author; // 作者
 - private DateTime date; // 更新/創(chuàng)建 日期
 - private string memo; // 備注
 - // 構(gòu)造函數(shù),構(gòu)造函數(shù)的參數(shù)在特性中也稱為“位置參數(shù)”。
 - public RecordAttribute(string recordType, string author, string date) {
 - this.recordType = recordType;
 - this.author = author;
 - this.date = Convert.ToDateTime(date);
 - }
 - // 對于位置參數(shù),通常只提供get訪問器
 - public string RecordType { get { return recordType; } }
 - public string Author { get { return author; } }
 - public DateTime Date { get { return date; } }
 - // 構(gòu)建一個屬性,在特性中也叫“命名參數(shù)”
 - public string Memo {
 - get { return memo; }
 - set { memo = value; }
 - }
 - }
 
注意構(gòu)造函數(shù)的參數(shù) date,必須為一個常量、Type類型、或者是常量數(shù)組,所以不能直接傳遞DateTime類型。
這個類不光看上去,實(shí)際上也和普通的類沒有任何區(qū)別,顯然不能它因為名字后面跟了個Attribute就搖身一變成了特性。那么怎樣才能讓它稱為特性并應(yīng)用到一個類上面呢?進(jìn)行下一步之前,我們看看.Net內(nèi)置的特性O(shè)bsolete是如何定義的:
- namespace System {
 - [Serializable]
 - [AttributeUsage(6140, Inherited = false)]
 - [ComVisible(true)]
 - public sealed class ObsoleteAttribute : Attribute {
 - public ObsoleteAttribute();
 - public ObsoleteAttribute(string message);
 - public ObsoleteAttribute(string message, bool error);
 - public bool IsError { get; }
 - public string Message { get; }
 - }
 - }
 
首先,我們應(yīng)該發(fā)現(xiàn),它繼承自Attribute類,這說明我們的 RecordAttribute 也應(yīng)該繼承自Attribute類。 (一個特性類與普通類的區(qū)別是:繼承了Attribute類)
其次,我們發(fā)現(xiàn)在這個特性的定義上,又用了三個特性去描述它。這三個特性分別是:Serializable、AttributeUsage 和 ComVisible。Serializable特性我們前面已經(jīng)講述過,ComVisible簡單來說是“控制程序集中個別托管類型、成員或所有類型對 COM 的可訪問性”(微軟給的定義)這里我們應(yīng)該注意到:特性本身就是用來描述數(shù)據(jù)的元數(shù)據(jù),而這三個特性又用來描述特性,所以它們可以認(rèn)為是“元數(shù)據(jù)的元數(shù)據(jù)”(元元數(shù)據(jù):meta-metadata)。(從這里我們可以看出,特性類本身也可以用除自身以外的其它特性來描述,所以這個特性類的特性是元元數(shù)據(jù)。)
因為我們需要使用“元元數(shù)據(jù)”去描述我們定義的特性 RecordAttribute,所以現(xiàn)在我們需要首先了解一下“元元數(shù)據(jù)”。這里應(yīng)該記得“元元數(shù)據(jù)”也是一個特性,大多數(shù)情況下,我們只需要掌握 AttributeUsage就可以了,所以現(xiàn)在就研究一下它。我們首先看上面AttributeUsage是如何加載到ObsoleteAttribute特性上面的。
[AttributeUsage(6140, Inherited = false)]
然后我們看一下AttributeUsage的定義:
- namespace System {
 - public sealed class AttributeUsageAttribute : Attribute {
 - public AttributeUsageAttribute(AttributeTargets validOn);
 - public bool AllowMultiple { get; set; }
 - public bool Inherited { get; set; }
 - public AttributeTargets ValidOn { get; }
 - }
 - }
 
可以看到,它有一個構(gòu)造函數(shù),這個構(gòu)造函數(shù)含有一個AttributeTargets類型的位置參數(shù)(Positional Parameter) validOn,還有兩個命名參數(shù)(Named Parameter)。注意ValidOn屬性不是一個命名參數(shù),因為它不包含set訪問器,(是位置參數(shù))。
這里大家一定疑惑為什么會這樣劃分參數(shù),這和特性的使用是相關(guān)的。假如AttributeUsageAttribute 是一個普通的類,我們一定是這樣使用的:
- // 實(shí)例化一個 AttributeUsageAttribute 類
 - AttributeUsageAttribute usage=new AttributeUsageAttribute(AttributeTargets.Class);
 - usage.AllowMultiple = true; // 設(shè)置AllowMutiple屬性
 - usage.Inherited = false;// 設(shè)置Inherited屬性
 
但是,特性只寫成一行代碼,然后緊靠其所應(yīng)用的類型(目標(biāo)類型),那么怎么辦呢?微軟的軟件工程師們就想到了這樣的辦法:不管是構(gòu)造函數(shù)的參數(shù) 還是 屬性,統(tǒng)統(tǒng)寫到構(gòu)造函數(shù)的圓括號中,對于構(gòu)造函數(shù)的參數(shù),必須按照構(gòu)造函數(shù)參數(shù)的順序和類型;對于屬性,采用“屬性=值”這樣的格式,它們之間用逗號分隔。于是上面的代碼就減縮成了這樣:
- [AttributeUsage(AttributeTargets.Class, AllowMutiple=true, Inherited=false)]
 
可以看出,AttributeTargets.Class是構(gòu)造函數(shù)參數(shù)(位置參數(shù)),而AllowMutiple 和 Inherited實(shí)際上是屬性(命名參數(shù))。命名參數(shù)是可選的。將來我們的RecordAttribute的使用方式于此相同。(為什么管他們叫參數(shù),我猜想是因為它們的使用方式看上去更像是方法的參數(shù)吧。)假設(shè)現(xiàn)在我們的RecordAttribute已經(jīng)OK了,則它的使用應(yīng)該是這樣的:
- [RecordAttribute("創(chuàng)建","張子陽","2008-1-15",Memo="這個類僅供演示")]
 - public class DemoClass{
 - // ClassBody
 - }
 - //其中recordType, author 和 date 是位置參數(shù),Memo是命名參數(shù)。
 
從AttributeUsage特性的名稱上就可以看出它用于描述特性的使用方式。具體來說,首先應(yīng)該是其所標(biāo)記的特性可以應(yīng)用于哪些類型或者對象。從上面的代碼,我們看到AttributeUsage特性的構(gòu)造函數(shù)接受一個 AttributeTargets 類型的參數(shù),那么我們現(xiàn)在就來了解一下AttributeTargets。
AttributeTargets 是一個位標(biāo)記,它定義了特性可以應(yīng)用的類型和對象。
- public enum AttributeTargets {
 - Assembly = 1, //可以對程序集應(yīng)用屬性。
 - Module = 2, //可以對模塊應(yīng)用屬性。
 - Class = 4, //可以對類應(yīng)用屬性。
 - Struct = 8, //可以對結(jié)構(gòu)應(yīng)用屬性,即值類型。
 - Enum = 16, //可以對枚舉應(yīng)用屬性。
 - Constructor = 32, //可以對構(gòu)造函數(shù)應(yīng)用屬性。
 - Method = 64, //可以對方法應(yīng)用屬性。
 - Property = 128, //可以對屬性 (Property) 應(yīng)用屬性 (Attribute)。
 - Field = 256, //可以對字段應(yīng)用屬性。
 - Event = 512, //可以對事件應(yīng)用屬性。
 - Interface = 1024, //可以對接口應(yīng)用屬性。
 - Parameter = 2048, //可以對參數(shù)應(yīng)用屬性。
 - Delegate = 4096, //可以對委托應(yīng)用屬性。
 - ReturnValue = 8192, //可以對返回值應(yīng)用屬性。
 - GenericParameter = 16384, //可以對泛型參數(shù)應(yīng)用屬性。
 - All = 32767, //可以對任何應(yīng)用程序元素應(yīng)用屬性。
 - }
 
因為AttributeUsage是一個位標(biāo)記,所以可以使用按位或“|”來進(jìn)行組合。所以,當(dāng)我們這樣寫時:
[AttributeUsage(AttributeTargets.Class|AttributeTargets.Interface)
意味著既可以將特性應(yīng)用到類上,也可以應(yīng)用到接口上。
AllowMutiple 屬性用于設(shè)置該特性是不是可以重復(fù)地添加到一個類型上(默認(rèn)為false),就好像這樣:
- [RecordAttribute("更新","jayce","2016-1-20")]
 - [RecordAttribute("創(chuàng)建","pop","2016-1-15",Memo="這個類僅供演示")]
 - public class DemoClass{
 - // ClassBody
 - }
 
Inherited 就更復(fù)雜一些了,假如有一個類繼承自我們的DemoClass,那么當(dāng)我們將RecordAttribute添加到DemoClass上時,DemoClass的子類也會獲得該特性。而當(dāng)特性應(yīng)用于一個方法,如果繼承自該類的子類將這個方法覆蓋,那么Inherited則用于說明是否子類方法是否繼承這個特性。
現(xiàn)在實(shí)現(xiàn)RecordAttribute應(yīng)該是非常容易了,對于類的主體不需要做任何的修改,我們只需要讓它繼承自Attribute基類,同時使用AttributeUsage特性標(biāo)記一下它就可以了(假定我們希望可以對類和方法應(yīng)用此特性):
- [AttributeUsage(AttributeTargets.Class|AttributeTargets.Method, AllowMultiple=true, Inherited=false)]
 - public class RecordAttribute:Attribute {
 - // 略
 - }
 
我們已經(jīng)創(chuàng)建好了自己的自定義特性,現(xiàn)在是時候使用它了。
- [Record("更新", "code", "2016-1-20", Memo = "修改 ToString()方法")]
 - [Record("更新", "jayce", "2016-1-18")]
 - [Record("創(chuàng)建", "pop", "2016-1-15")]
 - public class DemoClass {
 - public override string ToString() {
 - return "This is a demo class";
 - }
 - }
 - class Program {
 - static void Main(string[] args) {
 - DemoClass demo = new DemoClass();
 - Console.WriteLine(demo.ToString());
 - }
 - }
 
利用反射來查看 自定義特性信息 與 查看其他信息 類似,首先基于類型(本例中是DemoClass)獲取一個Type對象,然后調(diào)用Type對象的GetCustomAttributes()方法,獲取應(yīng)用于該類型上的特性。當(dāng)指定GetCustomAttributes(Type attributeType, bool inherit) 中的***個參數(shù)attributeType時,將只返回指定類型的特性,否則將返回全部特性;第二個參數(shù)指定是否搜索該成員的繼承鏈以查找這些屬性。
- class Program {
 - static void Main(string[] args) {
 - Type t = typeof(DemoClass);
 - Console.WriteLine("下面列出應(yīng)用于 {0} 的RecordAttribute屬性:" , t);
 - // 獲取所有的RecordAttributes特性
 - object[] records = t.GetCustomAttributes(typeof(RecordAttribute), false);
 - foreach (RecordAttribute record in records) {
 - Console.WriteLine(" {0}", record);
 - Console.WriteLine(" 類型:{0}", record.RecordType);
 - Console.WriteLine(" 作者:{0}", record.Author);
 - Console.WriteLine(" 日期:{0}", record.Date.ToShortDateString());
 - if(!String.IsNullOrEmpty(record.Memo)){
 - Console.WriteLine(" 備注:{0}",record.Memo);
 - }
 - }
 - }
 - }
 















 
 
 
 
 
 
 