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

來源:互聯網
上載者:User
文章目錄
  • 7.1 反射
  • 7.2 後期綁定
  • 7.3 attribute
  • 7.3.5 .NET Framework中的一些標準attribute
  • 7.3.6 自訂的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中的類之間使用後期綁定。這種情況我們將在稍後談到動態構造程式集的時候介紹。

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

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

  • 會失去由編譯器完成的語法驗證的好處。
  • 後期綁定的效能遠不如早期或動態Binder 方法。(即使使用了後面提到的最佳化方法。)
  • 無法為被混淆的類建立後期綁定。事實上,在混淆過程中,程式集中包含的類的名稱會被改變。因此,後期綁定機制無法正確地找到合適的類。
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 attribute7.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的另一個用處。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.