C#與NET實戰 第七章 反射、後期綁定與attribute 節選

23:07:28

我們在2.2.2節曾討論過元數據(metadata)以及它在程序集中的物理存儲方式。本章將會看到它們是如何構成反射與attribute機制的基礎的。

7.1 反射

反射機制代表了在執行期一個程序集的類型元數據的使用。通常情況下,該程序集是在另一個程序集執行的時候被顯式載入的,不過它也可以被動態生成。

反射這個詞用於表明我們使用了一個程序集的映像(就像鏡子中的映像)。該映像由程序集的類型元數據構成。我們有時候也會使用內省(introspection)這個術語來表示反射。

7.1.1 何時需要反射

我們收集了一些反射機制的使用分類,在本章接下來的小節中將對它們展開更詳細的討論。反射機制可以在下述場景中用到。

  • 在應用程序執行時,我們可以使用類型元數據的動態分析來探索程序集中的類型。例如,ildasm. exe與Reflector工具會顯式地裝載一個程序集中的模塊並分析它們的內容(參見2.3節)。
  • 使用後期綁定的期間。該技術需要使用程序集中的一個類,而該類在編譯期是未知的。後期綁定技術通常用於解釋型語言,如腳本語言。
  • 當我們希望使用attribute中的信息的時候。
  • 當我們希望從類外部訪問類中的非公開成員的時候。當然,這種行爲應該儘量避免,不過有時候還是有必要使用的。例如,在編寫沒有非公開成員就無法完成的單元測試的時候。
  • 在動態構造程序集的期間。爲了使用一個動態構造的程序集中的類,我們必須顯式地使用後期綁定技術。

CLR與Framework在某些情況下會使用反射機制。例如,在值類型的Equals()方法的默認實現中,使用反射逐一比較兩個實例中的字段。

CLR在序列化對象期間也使用反射,以確定哪個字段需要被序列化。甚至垃圾收集器也會在回收過程中使用它來構造引用樹。

7.1.2 .NET反射有何新意

反射機制的底層原理並不是什麼新概念。很早以前我們就能夠動態地分析一個可執行程序了,尤其是通過使用自描述信息。TLB格式(參見8.4節)就是爲了這個目的構想出來的。而TLB格式中的數據正是來自於IDL(Interface Definition Language,接口定義語言)格式。IDL語言也可以被視爲一種自描述語言。.NET中的反射機制則比TLB與IDL格式更進了一步。

  • 在某些基類的幫助下,它更易於使用。
  • 它比TLB與IDL語言更抽象。例如,它不使用物理地址,這意味着它在32位與64位機器上都能發揮作用。
  • 相比TBL元數據,.NET元數據總是包含在它所描述的模塊中。
  • 它描述數據的詳細程度遠勝於TLB格式。具體來說,我們能夠獲得一個程序集中聲明的任何類型的所有可能信息(例如:類中方法的某個參數的類型)。

.NET反射之所以能描述如此詳細的內容,歸功於.NET Framework 中衆多的基類,通過它們可以從一個包含在AppDomian中的程序集中抽取出各種類型元數據(type metadata)並使用它們。這些類大多可以在System.Reflection命名空間中找到,而且程序集中每個類型的元素都有一個類與之對應。

  • 有一個類的實例代表了程序集(System.Reflection.Assembly);
  • 有一個類的實例代表了類與結構(System.Type);
  • 有一個類的實例代表了方法(System.Reflection.MethodInfo);
  • 有一個類的實例代表了字段(System.Reflection.FieldInfo);
  • 有一個類的實例代表了方法參數(System.Reflection.ParameterInfo)。

……

最後要提醒大家,這些類僅僅提供了一種從邏輯上查看全體類型元數據的方法。但是我們看到的內容與物理的全體類型元數據並不會完全吻合,其中某些用於程序集內部組織的元素並沒有被展現出來。

System.Reflection命名空間下的所有類以一種邏輯方式相互聯繫。例如,從一個System. Reflection.Assembly的實例中,可以獲得一個System.Type實例的列表。從一個System.Type的實例中,可以獲得一個System.Reflection.MethodInfo實例表。而從一個System.Reflection.Method- Info實例中,又可以獲得一個System.Reflection.ParameterInfo實例表。所有這些如圖7-1所示。

圖7-1 反射類之間的交互

你可能會發現我們無法深入到IL指令層面,而只能到達字節表的層面,在字節表中包含一個方法的IL體。因此,爲了查看IL指令,你可能想使用某些庫,比如Cecil(由Jean-Baptiste Evain開發)、ILReader (由Lutz Roeder開發)或Rail(由Coimbra大學開發)。應該瞭解,在.NET 2下,反射知道如何處理泛型類型、泛型方法以及對參數類型的約束(參見13.10.2節)。

7.1.3 對載入AppDomain的程序集的反射

下面的例子展現瞭如何使用System.Reflection命名空間下的類分析類型元數據。事實上,這裏我們展現了一個分析它自身的類型元數據的程序集。爲了獲得一個代表該程序集的Assembly類的實例,我們使用了靜態方法Assembly.GetExecutingAssembly()。

例7-1

程序輸出如下:

7.1.4 從元數據獲取信息

本節的目的在於展示一個小程序,該程序使用反射機制顯示了包含在System與mscorlib程序集中的異常類集合。我們是建立在所有異常類都繼承自System.Exception這一事實的基礎上的。不是直接繼承自System.Exception的異常類型會用一個星號標記。

我們可以依據所有attribute類都繼承自System.Attribute類這一事實,很容易地修改該程序來顯示框架中所有的attribute類。

例7-2

注意前一個例子中使用的Assembly.ReflectionOnlyLoad()方法。通過該方法可以告訴CLR,載入的程序集將僅用於反射。因此,CLR將不允許以這種方法載入的程序集的代碼執行。同樣,以Reflection Only模式裝載程序集會稍微快些,因爲CLR不需要完成任何安全驗證工作。Assembly類提供bool ReflectionOnly{get;}屬性用於判斷一個程序集是否是通過該方法載入的。

7.2 後期綁定

在開始本節之前,建議對面向對象編程的基本內容有着很好的理解,尤其是有關多態的概念。該主題請參見第12章。

7.2.1 “綁定一個類”的含義

首先,我們需要在“綁定一個類”的含義上達成共識。我們將使用“軟件層”這個術語而不是“程序集”,因爲後者在其他技術中也被用到。

在使用類(實例化類並使用類的實例)的軟件層與定義類的軟件層之間會建立起一個類的聯繫。具體來說,這個聯繫是使用類的軟件層中對類中方法的調用與這些方法在定義類的軟件層中的物理地址之間的對應關係。正是通過這個聯繫,當類方法被調用時線程才能繼續執行下去。

一般來說,我們將一個類的聯繫分爲三類:完全在編譯期創建的早期綁定,在編譯期創建了部分聯繫的動態綁定以及在執行期創建的後期綁定。

7.2.2 早期綁定與動態綁定

早期綁定聯繫由編譯器在根據.NET源代碼創建程序集的期間創建。我們無法爲一個虛方法或抽象方法創建一個早期綁定。事實上,當一個虛方法或抽象方法被調用時,多態機制會在執行期間根據被調用方法的實際對象確定將要執行的代碼。這種情況下,該聯繫被視爲動態綁定。在其他資料中,動態綁定有時被稱爲隱式的後期綁定,因爲它們是由多態機制隱式創建的並且是在執行期完成的。

現在讓我們進一步觀察早期綁定,它們是爲靜態方法或類中那些不是虛方法或抽象方法的方法創建的。如果嚴格遵循前一節中對“類的聯繫”的定義,.NET中是不存在早期綁定的。事實上,我們必須先等待JIT編譯器將方法體轉換爲機器語言,才能知道它在進程地址空間中的物理地址。創建程序集的編譯器並不知道這個方法的地址信息[1]。我們在4.6節看到,爲了解決這個問題,創建程序集的編譯器在IL代碼中方法將被調用的位置插入了與被調用的方法相對應的元數據符號(metadata token) 。當方法體被即時(JIT)編譯的時候,CLR在內部保存了方法與機器語言下的方法體的物理地址的對應聯繫。這段被稱爲存根的信息被物理保存到一個與方法相關的內存地址中。

以上的認識很重要,因爲在像C++這樣的語言中,當一個方法不是虛方法或抽象方法(即C++中的純虛函數)時,編譯器就可以計算出該方法體在機器語言下的物理地址。然後,編譯器在每個調用該方法的位置插入一個指向該內存地址的指針。這個區別給了.NET很大的優勢,因爲編譯器不需要再考慮諸如內存表現之類的技術細節。IL代碼完全獨立於它所運行的物理層。

而在動態綁定中,其中幾乎所有的事物都是以與早期綁定中相同的方式工作的。編譯器在IL代碼中方法被調用的位置插入了與被調用的虛(或抽象)方法相對應的元數據符號。這裏,我們提到的元數據符號是屬於定義在引用類型中的方法的,而該引用類型就是將發生方法調用的那個類型。然後就是CLR的工作,它將在執行期間根據引用對象的具體實現確定跳轉到哪個方法。

插入一個類型元數據符號,編譯器使用這項技術創建動態綁定與早期綁定,它主要用於以下三種情況。

  • 當包含在模塊中的代碼調用了一個處在同一模塊中的方法時。
  • 當包含在模塊中的代碼調用了一個處在同一程序集的不同模塊中的方法時。
  • 當包含在程序集的一個模塊中的代碼調用了定義在另一個在編譯期引用進來的程序集中的方法時。在運行時,如果即時編譯方法調用的時候該程序集尚未載入,那麼CLR將隱式地裝載它。

7.2.3 後期綁定

程序集A的代碼可以實例化並使用一個定義在程序集B中的類型,而該類型可能在A編譯的時候並沒有被引用。我們把這種類型的聯繫描述爲後期綁定。我們之所以在句中使用“後期”這個詞是因爲綁定是在代碼執行期完成的而非編譯期。這種類型的綁定同樣是顯式的,因爲被調用的方法的名稱必須使用一個字符串顯式地指定。

後期綁定在微軟的開發世界中並不是什麼新的概念。COM技術中的自動化機制就是一個例子,它使用IDispatch接口作爲變通方法,允許腳本語言或者弱類型的語言如VB使用後期綁定。後期綁定的概念同樣存在於Java中。

後期綁定是習慣於C++的開發者很難理解的概念之一。事實上,在C++中只存在早期與動態綁定。難於理解的原因來自以下事實:我們知道創建一個綁定所需的必要信息(即元數據符號)處在將被調用的類所在的程序集B之中,但是我們不能理解爲什麼開發者不能利用編譯器的能力,在編譯A的期間,通過引用程序集B創建早期與動態綁定。對此,存在以下解釋:

  • 最常見的原因在於某些語言根本就沒有編譯器!在一個腳本語言中,指令是被一條一條解釋的。在這種情況下,只存在後期綁定。通過使用後期綁定,可以使用由解釋型語言編譯的程序集中的類。在.NET中可以方便地使用後期綁定技術這一事實,使得創建一個專有的解釋/動態語言變得相對容易(比如IronPython語言http://www.ironpython.com/)。
  • 我們可能希望在由編譯型語言如C#寫成的程序中使用後期綁定技術。原因在於,使用後期綁定可以爲應用程序的通用架構帶來某種程度的靈活性。該技術實際上是一種最近很流行的被稱爲插件的設計模式,我們將在本章對它做進一步介紹。
  • 某些應用程序需要調用尚未獲得的程序集中的代碼。一個典型的例子就是開源工具NUnit,該工具可以通過調用任意一個程序集的方法來測試其代碼。我們將在稍後構造一個自定義attribute的時候,再進一步接觸這個話題。
  • 如果在程序集A編譯期間程序集B尚不存在,我們就必須在A中的代碼與B中的類之間使用後期綁定。這種情況我們將在稍後談到動態構造程序集的時候介紹。

一些人喜歡使用後期綁定來代替多態。事實上,因爲在調用期間,只考慮方法的名稱與簽名式,而與被調用方法所處對象的類型無關,所以只需要在實現對象的時候提供具有合適名稱和簽名式的方法即可。但是,我個人不推薦這種做法,因爲它的約束性太差,並且無法促使應用程序的開發者去做恰當的設計以及使用抽象接口。

除了剛提到的原因外,還有就是不需要顯式地使用後期綁定。不要爲了好玩而在應用程序中使用後期綁定,因爲:

  • 會失去由編譯器完成的語法驗證的好處。
  • 後期綁定的性能遠不如早期或動態綁定方法。(即使使用了後面提到的優化方法。)
  • 無法爲被混淆的類創建後期綁定。事實上,在混淆過程中,程序集中包含的類的名稱會被改變。因此,後期綁定機制無法正確地找到合適的類。
7.2.4 在C#編譯到IL期間如何實例化一個未知的類

如果一個類或結構在編譯期是未知的,那麼就無法使用new操作符對它實例化。幸運的是,.NET Framework確實提供了一些類,使用這些類可以創建那些在編譯期間未知的類的實例。

1. 精確化一個類型

現在讓我們看一下,在指定一個類型時,可以採取的各種不同的技術。

  • 某些類的某些方法接受一個包含類型完整名稱(包括命名空間)的字符串。
  • 其他方法接受一個System.Type類的實例。在一個AppDomain中,每個System.Type類的實例代表一種類型,而且不會有兩個實例同時代表該類型。
    獲得一個System.Type類的實例的幾種方式:
  • 在C#中,我們通常使用typeof()關鍵字,它接受一個類型作爲參數並返回相應的System.Type實例。
  • 也可以使用System.Type類中GetType()靜態方法的一個重載版本。
  • 如果一個類型被封裝在另一個類中,可以使用System.Type類的非靜態方法GetNestedType() 或GetNestedTypes()。還可以使用System.Reflection.Assembly類的非靜態方法GetType()、 GetTypes()或GetExportedTypes()。
  • 也可以使用System.Relection.Module類的非靜態方法GetType()、GetTypes()或FindTypes()。

現在假設以下程序被編譯爲Foo.dll程序集。我們將要展示幾種創建一個NMFoo.Calc類的實例的方法,這些方法允許在一個沒有引用Foo.dll的程序集中完成創建。

例7-3 Foo.dll程序集的代碼

2. 使用System.Activator

System.Activator類提供了兩個靜態方法CreateInstance()與CreateInstanceFrom(),通過它們可以創建一個在編譯期間未知的類的實例。例如:

例7-4

這兩個方法中都提供了一些重載版本,甚至還有泛型版本,其中使用了以下參數。

  • 用於代表一個字符串的類或System.Type的一個實例;
  • 包含類的程序集的名稱,該參數是可選的;
  • 構造參數列表,該參數是可選的。

如果包含類的程序集並未出現在AppDomain中,調用CreateInstance()或CreateInstanceFrom()方法會導致該程序集被載入。取決於我們調用的是CreateInstance()方法還是CreateInstanceFrom()方法,在內部會調用System.AppDomain.Load()或System.AppDomain.LoadFrom()方法來載入程序集。CLR會根據提供的參數選擇類的一個構造函數,並返回一個包含了一個封送對象的ObjectHandle類的實例。在介紹.NET remoting的第22章中,我們會在分佈式應用的環境下展示這些方法的另一種用法。

使用System.Type類的一個實例來指定類型的CreateInstance()重載版本會直接返回對象的一個實例。

System.Activator還有一個CreateComInstanceFrom()方法,該方法用於創建一個COM對象的實例,以及一個用於創建遠程對象的GetObject()方法。

3. 使用System.AppDomain類

System.AppDomain類擁有CreateInstance()、CreateInstanceAndUnWrap()、CreateInstance- From()與CreateInstanceFromAndUnwrap()這四個非靜態方法,通過它們可以創建一個在編譯期間未知的類的實例,例如:

例7-5

這些方法與之前談到的System.Activator中的方法類似。不過,通過它們可以選擇將對象創建在哪個AppDomain中。此外,“AndUnwarp()”版本會返回一個對對象的直接引用,該引用是從一個ObjectHandle類的實例中獲得的。

4. 使用System.Reflection.ConstructorInfo

System.Reflection.ConstructorInfo類的實例引用一個構造函數。該類的Invoke()方法在內部爲構造函數創建了一個後期綁定,並通過這個綁定調用構造函數。因此,通過它們可以創建一個該構造函數所屬類型的實例。例如:

例7-6

5. 使用System.Type

通過System.Type類的非靜態方法InvokeMember()可以創建一個在編譯期間未知的類的實例,只需要在調用的時候使用BindingFlags枚舉量中的CreateInstance值即可。例如:

例7-7

6. 特殊情況

通過以上介紹的方法,幾乎可以創建任何一種類或結構的實例。下面是兩種特殊情況。

  • 爲了創建一個數組,必須調用System.Array類中的靜態方法CreateInstance()。
  • 爲了創建一個委託對象,必須調用System.Delegate類中的CreateDelegate()方法。
7.2.5 使用後期綁定

現在我們知道如何創建在編譯期間未知的類的實例,爲了使用這些實例,讓我們來看一下這些類型的成員之間的後期綁定的創建過程。同樣有幾種方法可以實現。

1. Type.InvokeMember()方法

讓我們回到Type.InvokeMember()方法,之前我們使用它通過調用未知的類型的一個構造函數創建了一個在編譯期間該未知的類型的實例。在內部實現中,該方法完成下面3個任務。

  • 它在它被調用的類型上尋找與所提供的信息相對應的成員。
  • 如果該成員被找到,爲它創建一個後期綁定。
  • 使用該成員(是方法就調用,是構造函數就創建一個對象的實例,是字段就讀取或設值,是屬性就執行set或get訪問器,等等)。

下面的例子展示瞭如何調用NMFoo.Calc類的實例上的Sum()方法(注意在調試期間,調試器能夠進入使用後期綁定的方法體中)。

例7-8

Type.InvokeMember()方法最常用的重載版本是:

invokeAttr參數是一個二進制標誌位,它指示了搜索何種類型的成員。爲了搜索方法,我們使用BindingFlags.InvokeMethod標誌位。各種標誌位的介紹請參見MSDN上名爲“BindingFlags Enumeration”的文章。

binder參數是一個Binder類型的對象,它會指示InvokeMember()方法如何搜索。大多數情況下,該參數可以設爲null以表示希望使用默認值,也就是System.Type.DefaultBinder。Binder類型的對象提供了以下類型的信息:

  • 它指示了參數會接受何種類型的轉換。在上一個例子中,我們可以提供兩個double類型的參數。由於DefaultBinder支持從double到int的轉換,所以仍然能夠成功地調用方法。
  • 它指示了我們是否在參數列表中使用了可選參數。

所有這些(尤其是類型轉換表)在MSDN上一篇名爲Type.DefaultBinder Property的文章中有更詳細的介紹。我們還可以通過繼承Binder類,創建自己的binder對象。不過在大多數情況下使用一個DefaultBinder的實例就足夠了。

如果在後期綁定成員的調用時引發了異常,InvokeMember()會截獲異常並重新拋出一個System.Reflection.TargetInvocation Exception類型的異常。自然地,在方法中引發的異常會被重新拋出的異常中的InnerException屬性所引用。

最後注意,在創建一個後期綁定時,無法訪問非公有成員。否則,通常會拋出System.Security. SecurityException異常。不過,如果System.Security.Permissions. ReflectionPermissionFlags的TypeInformation標誌位(可以通過System.Security.Permissions. ReflectionPermission類的實例訪問)被設爲真,就可以訪問非公有成員。如果MemberAccess標誌位被設爲真,就可以訪問非可見類型(即以非公有方式封裝在其他類型中)[2]

2. 一次綁定,多次調用

我們看到,通過一個ConstructorInfo實例可以創建一個後期綁定以調用一個構造函數,以同樣的方式,通過一個System.Reflection.MethodInfo類的實例也可以創建一個後期綁定並調用任意一個的方法。使用MethodInfo類而不是Type.InvokeMember()方法的優勢在於可以節省每次調用時搜索成員的時間,因此會帶來一些性能上的優化。如下例所示。

例7-9

3.VB.NET如何揹着你創建後期綁定

讓我們爲VB.NET做一些旁註,並觀察一下當Strict選項被設爲Off之後,該語言如何揹着你祕密地使用後期綁定。例如,下面的VB.NET程序……

例7-10 VB.NET與後期綁定

……等同於下面的C#程序:

例7-11

7.2.6 利用接口:使用後期綁定的正確方法

爲了使用一個在編譯期間未知的類,除了我們介紹過的通過使用後期綁定的方法外,還有另一個完全不同的方法。該方法具有較大優勢,因爲與使用早期或動態綁定相比,它在性能上幾乎沒有損失。不過,爲了使用該“祕訣”,你必須迫使自己遵循某種規範(實際上就是名爲插件的設計模式)。

我們的想法是確保在編譯時未知的類型實現了一個接口,而該接口是編譯器所知的。爲此,我們不得不創建第三個程序集,用於承載該接口。讓我們用三個程序集重寫Calc的例子:

例7-12 包含接口的程序集的代碼(InterfaceAsm.cs)

例7-13 包含目標類的程序集的代碼(ClassAsm.cs)

例7-14 在編譯期目標類未知的客戶程序集的代碼(ProgramAsm.cs)

注意,通過顯式地將CreateInstanceAndUnwrap()方法返回的對象強制轉換爲ICalc類型,就可以通過動態鏈接方式調用Sum()方法。我們還可以使用Activator.CreateInstance<ICalc>()這個泛型重載版本來避免類型轉化。

圖7-2對3個程序集的組織結構以及它們之間的聯繫做了總結。

 

圖7-2 插件設計模式與程序集組織結構

根據插件設計模式背後的思想,那些在CreateInstanceAndUnwrap()方法中用於創建實例所必需的數據(這裏是"Foo.dll"與"NMFoo.CalcWithInterface"兩個字符串)通常保存配置文件中。這樣,就可以通過修改配置文件來選擇新的實現而無需重新編譯。

插件設計模式的一個變種就是使用抽象類代替接口。

最後,應該知道還可以使用委託來創建一個方法的後期綁定。儘管該方法比使用MethodInfo更加高效,但是一般來說我們更喜歡使用插件設計模式。

7.3 attribute

7.3.1 attribute是什麼

一個attribute是一份標記代碼中的元素的信息,這個元素可以是一個類或一個方法。例如,.NET Framework 提供了System.ObsoleteAttribute,該attribute可用於標記一個方法,如下所示(注意使用[]括號的語法):

Fct()方法被標記上了System.ObsoleteAttribute信息,該信息會在編譯期間被插入到程序集中,以後可以被C#編譯器使用。當調用該方法時,編譯器會發出警告,提示最好避免調用廢棄方法,因爲此類方法可能在將來的版本中消失。如果沒有attribute,就不得不通過合適的文檔表述出Fct()方法現在處於廢棄狀態這一事實;而且這種做法的缺點在於,無法保證客戶會閱讀文檔從而知道該方法現在是廢棄的。

7.3.2 何時需要attribute

使用attribute的優勢在於它所包含的信息會被插入到程序集中,而這些信息可以在不同的時間用於各種不同目的:

  • attribute可以爲編譯器所用,剛介紹過的System.ObsoleteAttribute就是一個很好的例子。某些專門針對編譯器的標準attribute不會保存到程序集中。例如,SerializationAttribute並不會直接爲一個類型加上特定的標記,而只是告訴編譯器該類型可以被序列化。因此,編譯器在將被CLR在執行期間使用的具體類型上設置某些標誌。像SerializationAttribute這樣的attribute,也被稱爲僞attribute。
  • attribute可以在CLR執行期間使用。例如,.NET Framework提供了System.ThreadStatic- Attribute,當一個靜態字段被標記上該attribute時,CLR將確保在執行期間每個線程中只有該字段的一個版本。
  • attribute可以在調試器執行期間使用。因此,通過System.Diagnostics.DebuggerDisplay- Attribute可以在調試期間定製代碼中某個元素的顯示內容(例如某個對象的狀態)。
  • attribute可以被一個工具使用。例如,.NET Framework提供了System.Runtime.Interop- Services.ComVisibleAttribute,當一個類被標記上該attribute,tlbexp.exe 工具會爲該類產生一個文件,使得該類可以被當作一個COM對象使用。
  • attribute可以在用戶代碼執行期間使用,此時需要使用反射機制去訪問attribute信息。例如,使用attribute驗證類中字段的值就是一件很有趣的事。一個字段必須處於某個範圍內,一個引用字段必須非空,一個字符串字段最多包含100個字符,……由於存在反射機制,使得編寫代碼以驗證任何一個被標記字段的狀態變得很容易。稍後,我們將展示一個在代碼中使用attribute的例子。
  • attribute可以在用戶通過諸如ildasm.exe或Reflector這樣的工具分析程序集的時候使用。因此,可以想象一個attribute將會爲代碼中的一個元素賦予一個說明其特性的字符串。由於該字符串被包含在程序集中,所以我們就可以直接查閱這些註釋而無需訪問源代碼。
7.3.3 關於attribute應該知道的事
  • 一個attribute必須由一個繼承於System.Attribute的類定義。
  • attribute類的一個實例只有在被反射機制訪問時纔會被實例化。根據它的使用情況,一個attribute類不一定會被實例化(像System.ObsoleteAttribute那樣的attribute就不需要被反射機制使用)。
  • .NET Framework 內置了一些attribute以供使用。某些attribute是專門爲CLR提供的,其他的則被編譯器或微軟提供的工具所使用。
  • 可以創建自己的attribute類,不過它們只能被你的程序使用,因爲你無法修改編譯器或CLR。
  • 習慣上,一個attribute類的名稱會以Attribute爲後綴。不過,在C#中,一個名爲XXXAttribute的attribute,在標記代碼中元素的時候既可以使用XXXAttribute表示也可以簡單地用XXX表示。

在專門介紹泛型的那一章中,我們在13.10.3節中討論了attribute概念與泛型概念之間相互交疊的規則。

7.3.4 可以應用attribute的代碼元素

attribute將被應用於源代碼中的各種元素。下面是可以使用attribute標記的所有元素。它們是由AttributeTargets枚舉量的值定義的。

 

7.3.5 .NET Framework中的一些標準attribute

要想很好地理解一個attribute,首先要很好地瞭解它的應用場景。因此,每個標準attribute將在專門提到它們的章節中介紹。

一些與安全管理相關的attribute參見6.6節。

一些與P/Invoke機制相關的attribute參見8.1節。

一些與在.NET應用程序中使用COM相關的attribute參見原書8.4.3節。

一些與序列化機制相關的attribute參見22.3節。

一些與XML序列化相關的attribute參見原書21.9.2節。

允許實現一個同步機制的System.Runtime.Remoting.Contexts.SynchronizationAttribute參見5.9節。

允許提示編譯器對某些方法進行有條件編譯的ConditionalAttribute參見9.3.2節。

用於針對靜態字段修改線程行爲的ThreadStaticAttribute參見原書5.13.1節。

用於提示編譯器是否必須做某些驗證的CLSCompliantAttribute參見4.9.2節。

用於實現params C#關鍵字的ParamArrayAttribute參見4.9.2節。

CategoryAttribute參見18.5節。

7.3.6 自定義的attribute的示例

一個自定義的attribute就是你通過定義一個繼承於System.Attribute的類而爲自己創建的attribute。和我們在本節一開始談到的字段驗證attribute類似,可以想象,在很多情況下我們都可以從使用自定義的attribute中受益。我們將要展示的例子來自於NUnit開源工具的啓發。

通過NUnit工具可以執行任何程序集中的任何方法從而測試它們。由於沒有必要測試一個程序集中的每一個方法,NUnit僅執行那些被標記了TestAttribute的方法。

爲了實現該特性的一個簡化版,我們將做出以下約束。

  • 如果沒有拋出任何未捕獲的異常則認爲該方法通過測試。
  • 我們定義了一個只能應用於方法的TestAttribute。該attribute可以配置方法必須被執行的次數(通過int類型的TestAttribute.nTime屬性)。該attribute還可以用於忽略一個被標記的方法(通過bool類型的TestAttribute.Ignore屬性)。
  • Program.TestAssembly(Assembly)方法允許執行包含在程序集中所有被標記了TestAttribute的方法,而程序集是作爲參數傳入方法的。出於簡單化的考慮,我們假設這些方法都是公有的、非靜態的而且不接受任何參數。我們還必須使用後期綁定來訪問這些被標記的方法。

下面的程序滿足了這些約束。

例7-15

該程序輸出:

讓我們做一些註解。

  • 我們爲TestAttribute類標記了一個AttributeUsage類型的attribute。我們使用AttributeTarget枚舉量的Method值告訴編譯器TestAttribute只能被應用在方法上。
  • 我們將AttributeUsage類的AllowMultiple屬性設爲false以表示一個方法不會接受多個TestAttribute類型的attribute。注意用於初始化AllowMultiple屬性的特殊語法,我們把AllowMultiple稱爲有名參數。
  • 在爲Foo.CrashButIgnore()方法標記TestAttribute的時候,我們也使用了有名參數這一語法。
  • 當一個異常被引發並且沒有在某個方法執行期間被捕獲,但由於該方法是通過後期綁定調用的,所以此時調用該方法的方法會產生一個TargetInvocationException類型的異常將原始異常覆蓋,而原始異常則被覆蓋它的異常的InnerException屬性所引用。
  • 爲了避免將代碼分散到多個程序集中,該程序將進行自我測試(事實上,只有Foo類中的方法被測試,因爲只有它們被標記了TestAttribute)。圖7-3是將代碼分散後將會出現的程序集組織結構。

圖7-3 程序集組織結構

圖7-3與圖7-2相似不是偶然的。在兩種情況中,都是通過一個在編譯期間兩者都知道的中介(本例中是一個attribute,而前一個例子中是一個接口)來使用一個在編譯期未知的元素。

7.3.7 條件attribute

C#2引入了條件attribute的概念。一個條件attribute只有在一個符號被定義後纔會被編譯器考慮到。下面的例子展示了在一個由3個文件生成的項目中使用條件attribute。

例7-16

例7-17

例7-18

在上一節的示例中,條件attribute被用於從同一段代碼產生debug與release版本。在9.2.2節,我們介紹了條件attribute的另一個用處。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章