.Net 中的反射(反射特性) - Part.3

amadan發表於2021-09-09

.Net 中的反射(反射特性) - Part.3

反射特性(Attribute)

可能很多人還不瞭解特性,所以我們先了解一下什麼是特性。想想看如果有一個訊息系統,它存在這樣一個方法,用來將一則短訊息傳送給某人:

// title: 標題;author:作者;content:內容;receiverId:接受者Id
public bool SendMsg(string title, string author, string content, int receiverId){
    // Do Send Action
}

我們很快就發現這樣將引數一個個羅列到方法的引數列表中擴充套件性很糟糕,我們最好定義一個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
}

此時,我們或許應該將舊的方法刪除,用這個擴充套件性更好的SendMsg方法來取代。遺憾的是我們往往不能,因為這組程式可能作為一組API釋出,在很多客戶程式中已經在使用舊版本的SendMsg()方法,如果我們在更新程式的時候簡單地刪除掉舊的SendMsg()方法,那麼將造成使用老版本SendMsg()方法的客戶程式不能工作。

這個時候,我們該如果做呢?我們當然可以透過方法過載來完成,這樣就不用刪除舊的SendMsg()方法了。但是如果新的SendMsg()不僅最佳化了引數的傳遞,並且在演算法和效率上也進行了全面的最佳化,那麼我們將會迫切希望告知客戶程式現在有一個全新的高效能SendMsg()方法可供使用,但此時客戶程式並不知道已經存在一個新的SendMsg方法,我們又該如何做呢?我們可以打電話告訴維護客戶程式的程式設計師,或者發電子郵件給他,但這樣顯然不夠方便,最好有一種辦法能讓他一編譯專案,只要存在對舊版本SendMsg()方法的呼叫,就會被編譯器告知。

1..Net內建特性介紹

.Net 中可以使用特性來完成這一工作。特性是一個物件,它可以載入到程式集及程式集的物件中,這些物件包括 程式集本身、模組、類、介面、結構、建構函式、方法、方法引數等,載入了特性的物件稱作特性的目標。特性是為程式新增後設資料(描述資料的資料)的一種機制,透過它可以給編譯器提供指示或者提供對資料的說明。

NOTE:特性的英文名稱叫做Attribute,在有的書中,將它翻譯為“屬性”;另一些書中,將它翻譯為“特性”;由於通常我們將含有get和/或set訪問器的類成員稱為“屬性”(英文Property),所以本文中我將使用“特性”這個名詞,以區分“屬性”(Property)。  
    中文版的VS2005使用“屬性”。

1.1 System.ObsoleteAttribute 特性

我們透過這個例子來看一下特性是如何解決上面的問題:我們可以給舊的SendMsg()方法上面加上Obsolete特性來告訴編譯器這個方法已經過時,然後當編譯器發現當程式中有地方在使用這個用Obsolete標記過的方法時,就會給出一個警告資訊。

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 voidMain(string[] args) {
           TestClass.ShowMsg();
           TestClass.ShowMsg(new Message());         
       }
    }
}

現在執行這段程式碼,我們會發現編譯器給出了一個警告:警告CS0618: “Attribute.TestClass.ShowMsg()”已過時:“請使用新的SendMsg(Message msg)過載方法”。透過使用特性,我們可以看到編譯器給出了警告資訊,告訴客戶程式存在一個新的方法可供使用,這樣,程式設計師在看到這個警告資訊後,便會考慮使用新的SendMsg()方法。

NOTE:簡單起見,TestClass類和 Program位於同一個程式集中,實際上它們可以離得很遠。

1.2 特性的使用方法

透過上面的例子,我們已經大致看到特性的使用方法:首先是有一對方括號“[]”,在左方括號“[”後緊跟特性的名稱,比如Obsolete,隨後是一個圓括號“()”。和普通的類不同,這個圓括號不光可以寫入建構函式的引數,還可以給類的屬性賦值,在Obsolete的例子中,僅傳遞了建構函式引數。

NOTE:實際上,當你用滑鼠框選住Obsolete,然後按下F12轉到定義,會發現它的全名是ObsoleteAttribute,繼承自Attribute類。但是這裡卻僅用Obsolete來標記方法,這是.Net的一個約定,所有的特性應該均以Attribute來結尾,在為物件標記特性時如果沒有新增Attribute,編譯器會自動尋找帶有Attribute的版本。

NOTE:使用建構函式引數,引數的順序必須同建構函式宣告時的順序相同,所有在特性中也叫位置引數(Positional Parameters),與此相應,屬性引數也叫做命名引數(Named Parameters)。在下面會詳細說明。

2.自定義特性(Custom Attributes)

2.1 範例介紹

如果不能自己定義一個特性並使用它,我想你怎麼也不能很好的理解特性,我們現在就自己構建一個特性。假設我們有這樣一個很常見的需求:我們在建立或者更新一個類檔案時,需要說明這個類是什麼時候、由誰建立的,在以後的更新中還要說明在什麼時候由誰更新的,可以記錄也可以不記錄更新的內容,以往你會怎麼做呢?是不是像這樣在類的上面給類新增註釋:

//更新:Matthew,2008-2-10, 修改 ToString()方法
//更新:Jimmy,2008-1-18
//建立:張子陽,2008-1-15
public class DemoClass{
    // Class Body
}

這樣的的確確是可以記錄下來,但是如果有一天我們想將這些記錄儲存到資料庫中作以備份呢?你是不是要一個一個地去檢視原始檔,找出這些註釋,再一條條插入資料庫中呢?

透過上面特性的定義,我們知道特性可以用於給型別新增後設資料,這些後設資料可以用於描述型別。那麼在此處,特性應該會派上用場。那麼在本例中,後設資料應該是:註釋型別(“更新”或者“建立”),修改人,日期,備註資訊(可有可無)。而特性的目標型別是DemoClass類。

按照對於附加到DemoClass類上的後設資料的理解,我們先建立一個封裝了後設資料的類RecordAttribute:

public class RecordAttribute {
    private string recordType;      // 記錄型別:更新/建立
    private string author;          // 作者
    private DateTime date;          // 更新/建立 日期
    private string memo;         // 備註

    // 建構函式,建構函式的引數在特性中也稱為“位置引數”。
    public RecordAttribute(string recordType, string author, string date) {
       this.recordType = recordType;
       this.author = author;
       this.date = Convert.ToDateTime(date);
    }

    // 對於位置引數,通常只提供get訪問器
    public string RecordType {   get { return recordType; }   }
    public string Author { get { return author; } }
    public DateTime Date { get { return date; } }

    // 構建一個屬性,在特性中也叫“命名引數”
    public string Memo {
       get { return memo; }
       set { memo = value; }
    }
}

NOTE:注意建構函式的引數 date,必須為一個常量、Type型別、或者是常量陣列,所以不能直接傳遞DateTime型別。

這個類不光看上去,實際上也和普通的類沒有任何區別,顯然不能它因為名字後面跟了個Attribute就搖身一變成了特性。那麼怎樣才能讓它稱為特性並應用到一個類上面呢?進行下一步之前,我們看看.Net內建的特性Obsolete是如何定義的:

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; }
    }
}

2.2 新增特性的格式(位置引數和命名引數)

首先,我們應該發現,它繼承自Attribute類,這說明我們的RecordAttribute也應該繼承自Attribute類。

其次,我們發現在這個特性的定義上,又用了三個特性去描述它。這三個特性分別是:Serializable、AttributeUsage 和 ComVisible。Serializable特性我們前面已經講述過,ComVisible簡單來說是“控制程式集中個別託管型別、成員或所有型別對 COM 的可訪問性”(微軟給的定義)。這裡我們應該注意到:特性本身就是用來描述資料的後設資料,而這三個特性又用來描述特性,所以它們可以認為是“後設資料的後設資料”(元後設資料:meta-metadata)。

因為我們需要使用“元後設資料”去描述我們定義的特性 RecordAttribute,所以現在我們需要首先了解一下“元後設資料”。這裡應該記得“元後設資料”也是一個特性,大多數情況下,我們只需要掌握 AttributeUsage就可以了,所以現在就研究一下它。我們首先看上面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; }
    }
}

可以看到,它有一個建構函式,這個建構函式含有一個AttributeTargets型別的位置引數(Positional Parameter),還有兩個命名引數(Named Parameter)。注意ValidOn屬性不是一個命名引數,因為它不包含set訪問器。

這裡大家一定疑惑為什麼會這樣劃分引數,這和特性的使用是相關的。假如AttributeUsageAttribute 是一個普通的類,我們一定是這樣使用的:

// 例項化一個 AttributeUsageAttribute 類
AttributeUsageAttribute usage=new AttributeUsageAttribute(AttributeTargets.Class)
;
usage.AllowMultiple = true;  // 設定AllowMutiple屬性
usage.Inherited = false;// 設定Inherited屬性

但是,特性只寫成一行程式碼,然後緊靠其所應用的型別(目標型別),那麼怎麼辦呢?微軟的軟體工程師們就想到了這樣的辦法:不管是建構函式的引數 還是 屬性,統統寫到建構函式的圓括號中,對於建構函式的引數,必須按照建構函式引數的順序和型別;對於屬性,採用“屬性=值”這樣的格式,它們之間用逗號分隔。於是上面的程式碼就減縮成了這樣:

[AttributeUsage(AttributeTargets.Class, AllowMutiple=true, Inherited=false)]

可以看出,AttributeTargets.Class是建構函式引數(位置引數),而AllowMutiple 和 Inherited實際上是屬性(命名引數)。命名引數是可選的。將來我們的RecordAttribute的使用方式於此相同。(為什麼管他們叫引數,我猜想是因為它們的使用方式看上去更像是方法的引數吧。)

假設現在我們的RecordAttribute已經OK了,則它的使用應該是這樣的:

[RecordAttribute("建立","張子陽","2008-1-15",Memo="這個類僅供演示")]
public class DemoClass{
// ClassBody
}

其中recordType, author 和 date 是位置引數,Memo是命名引數。

2.3 AttributeTargets 位標記

從AttributeUsage特性的名稱上就可以看出它用於描述特性的使用方式。具體來說,首先應該是其所標記的特性可以應用於哪些型別或者物件。從上面的程式碼,我們看到AttributeUsage特性的建構函式接受一個 AttributeTargets 型別的引數,那麼我們現在就來了解一下AttributeTargets。

AttributeTargets 是一個位標記,它定義了特性可以應用的型別和物件。

[Flags]
public enum AttributeTargets {

    Assembly = 1,         //可以對程式集應用屬性。
    Module = 2,              //可以對模組應用屬性。
    Class = 4,            //可以對類應用屬性。
    Struct = 8,              //可以對結構應用屬性,即值型別。
    Enum = 16,            //可以對列舉應用屬性。
    Constructor = 32,     //可以對建構函式應用屬性。
    Method = 64,          //可以對方法應用屬性。
    Property = 128,           //可以對屬性 (Property) 應用屬性 (Attribute)。
    Field = 256,          //可以對欄位應用屬性。
    Event = 512,          //可以對事件應用屬性。
    Interface = 1024,            //可以對介面應用屬性。
    Parameter = 2048,            //可以對引數應用屬性。
    Delegate = 4096,             //可以對委託應用屬性。
    ReturnValue = 8192,             //可以對返回值應用屬性。
    GenericParameter = 16384,    //可以對泛型引數應用屬性。
    All = 32767,  //可以對任何應用程式元素應用屬性。
}

現在應該不難理解為什麼上面我範例中用的是:

[AttributeUsage(AttributeTargets.Class, AllowMutiple=true, Inherited=false)]

而ObsoleteAttribute特性上載入的 AttributeUsage是這樣的:

[AttributeUsage(6140, Inherited = false)]

因為AttributeUsage是一個位標記,所以可以使用按位或“|”來進行組合。所以,當我們這樣寫時:

[AttributeUsage(AttributeTargets.Class|AttributeTargets.Interface)

意味著既可以將特性應用到類上,也可以應用到介面上。

NOTE:這裡存在著兩個特例:觀察上面AttributeUsage的定義,說明特性還可以載入到程式集Assembly和模組Module上,而這兩個屬於我們的編譯結果,在程式中並不存在這樣的型別,我們該如何載入呢?可以使用這樣的語法:[assembly:SomeAttribute(parameter list)],另外這條語句必須位於程式語句開始之前。

2.4 Inherited 和 AllowMutiple屬性

AllowMutiple 屬性用於設定該特性是不是可以重複地新增到一個型別上(預設為false),就好像這樣:

[RecordAttribute("更新","Jimmy","2008-1-20")]
[RecordAttribute("建立","張子陽","2008-1-15",Memo="這個類僅供演示")]
public class DemoClass{
// ClassBody
}

所以,我們必須顯示的將AllowMutiple設定為True。

Inherited 就更復雜一些了,假如有一個類繼承自我們的DemoClass,那麼當我們將RecordAttribute新增到DemoClass上時,DemoClass的子類也會獲得該特性。而當特性應用於一個方法,如果繼承自該類的子類將這個方法覆蓋,那麼Inherited則用於說明是否子類方法是否繼承這個特性。

在我們的例子中,將 Inherited 設為false。

2.5 實現 RecordAttribute

現在實現RecordAttribute應該是非常容易了,對於類的主體不需要做任何的修改,我們只需要讓它繼承自Attribute基類,同時使用AttributeUsage特性標記一下它就可以了(假定我們希望可以對類和方法應用此特性):

[AttributeUsage(AttributeTargets.Class|AttributeTargets.Method, AllowMultiple=true, Inherited=false)]
public class RecordAttribute:Attribute {
    // 略
}

2.6 使用 RecordAttribute

我們已經建立好了自己的自定義特性,現在是時候使用它了。

[Record("更新", "Matthew", "2008-1-20", Memo = "修改 ToString()方法")]
[Record("更新", "Jimmy", "2008-1-18")]
[Record("建立", "張子陽", "2008-1-15")]
public class DemoClass {    
    public override string ToString() {
       return "This is a demo class";
    }
}

class Program {
    static voidMain(string[] args) {
       DemoClass demo = new DemoClass();
       Console.WriteLine(demo.ToString());
    }
}

這段程式簡單地在螢幕上輸出一個“This is a demo class”。我們的屬性也好像使用“//”來註釋一樣對程式沒有任何影響,實際上,我們新增的資料已經作為後設資料新增到了程式集中。可以透過IL DASM看到:

圖片描述

3.使用反射檢視自定義特性

利用反射來檢視 自定義特性資訊 與 檢視其他資訊 類似,首先基於型別(本例中是DemoClass)獲取一個Type物件,然後呼叫Type物件的GetCustomAttributes()方法,獲取應用於該型別上的特性。當指定GetCustomAttributes(Type attributeType, bool inherit) 中的第一個引數attributeType時,將只返回指定型別的特性,否則將返回全部特性;第二個引數指定是否搜尋該成員的繼承鏈以查詢這些屬性。

class Program {
    static voidMain(string[] args) {
       Type t = typeof(DemoClass);
       Console.WriteLine("下面列出應用於 {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);
           }
       }
    }
}

輸出為:

下面列出應用於 AttributeDemo.DemoClass 的RecordAttribute屬性:
   AttributeDemo.RecordAttribute
      型別:更新
      作者:Matthew
      日期:2008-1-20
      備註:修改 ToString()方法
   AttributeDemo.RecordAttribute
      型別:更新
      作者:Jimmy
      日期:2008-1-18
   AttributeDemo.RecordAttribute
      型別:建立
      作者:張子陽
      日期:2008-1-15

好了,到了這一步,我想將這些資料錄入資料庫中將不再是個問題,我們關於反射自定義特性的章節也就到此為止了。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/2310/viewspace-2811731/,如需轉載,請註明出處,否則將追究法律責任。

相關文章