使用
值得慶幸的是
把元數據綁定到可執行的程序集裡
關於
開發者通過使用自定義特性
我們經常提到一個類的屬性
通過這篇文章
公共語言運行時是如何使用特性的?
在你開始考慮如何使用你自己定義的特性類之前
[WebMethod]特性提供了一個簡單的例子
public class SomeWebService : System
{
[WebMethod]
public DataSet GetDailySales()
{
//處理請求的代碼
}
}
你只要把[WebMethod]特性添加到一個方法上
在給定的方法上使用[Conditional]特性
public class SomeClass
{
[Conditional(
public void UnitTest()
{
//單元測試代碼
}
}
這段代碼說明
特性使用了定位參數和命名參數
讓我們回到使用了[WebMethod]特性的例子
[WebMethod(Description =
命名參數是可選擇的
我將會在文章的後面講述更多關於定位參數和命名參數的內容
特性可用於運行時
在這篇文章裡
考慮在設計時
標准的
Table
Attribute
Description
Designer
指定用於為組件實現設計時服務的類。TW.WINgwIT.cOm
DefaultProperty
指定在屬性表單中,組件的默認的屬性。
Category
指定在屬性表單中,屬性的類別。
Description
指定在屬性表單中,有關屬性的描述。
這些與表單相關的特性,讓我們認識到,可以在設計時使用特性以及它們的值,就像在運行時一樣。
自定義特性vs.類的屬性
在特性和類的屬性之間存在著明顯相似的地方。這給我們何時,何處應該使用自定義特性帶來了困惑。開發者們通常引用一個類的屬性,並把屬性的值作為自己“特性”,那麼屬性和特性之間真正的區別在哪裡呢?
當你定義特性的時候,它和屬性沒有根本的區別,使用時,可以以相同的方式把它附加到程序集不同的類型上,而不僅僅在類上使用。Table2列舉了可以應用特性的所有程序集類型。
Table 2:可以應用特性的所有程序集類型。
Type
Assembly
Class
Delegate
Enum
Event
Interface
Method
Module
Parameter
Constructor
Field
Property
ReturnValue
Structure
讓我們從清單中挑選一個作為例子。你可以在參數上應用特性,這看起來很微小,好像是在給參數添加屬性?其實,這是一個新穎的,非常不錯的主意,因為你不會用類的屬性做這件事。這裡也突出了特性和屬性之間很大的不同之處,因為屬性僅僅是類的一個成員而已。它們不能與一個參數,或者清單中列舉的其他類型關聯起來,當然,這要把類排除在外。
在另外的方面,類的屬性被限制在運行的環境下,而特性卻沒有被限制。通過定義,一個屬性就依賴於特定的類,這個屬性僅僅可以通過類的實例訪問,或者通過該類派生類的實例訪問。另一方面,特性卻可以應用到任何地方。在assembly類型上應用特性,以檢驗是否與自定義特性中的相匹配,這對於assembly類型來說,是最適合的了。在下一部分,我將更多的討論自定義特性類中的ValidOn屬性。在面向組件的開發中,這是非常有用的,因為特性的這個特征將更加促進松耦合。
特性和屬性之間另外的一個不同的地方將涉及到它們各自存儲的值。屬性成員的值是一個實例化的值,在運行時,是可以被改變的。而特性的值,是在設計時(在源代碼裡)設定,然後直接把這些特性的值編譯成元數據保存到程序集裡。之後,你將不能改變這些特性的值。實際上,你已經把這些特性的值,變成硬編碼的、只讀的數據。
考慮一下,你應用特性的時候。舉個例子,在一個類定義的時候,給其附加了一個特性,那麼該類的每一個實例都會擁有相同的分配給此特性值,而不論你實例化該類的多少個實例。你不能把特性附加到一個類的實例上,你只可以在類型/類的定義上應用特性。
創建一個自定義特性類
現在,綜合以上的描述,我們將演示一個更實際的實現過程。讓我們創建一個自定義特性類。該特性會保存一些關於代碼修改的跟蹤信息,在源代碼裡,這些都將作為注釋。在這個例子裡,我們將僅僅記錄一些條目:缺陷id,開發者id,修改的日期,導致缺陷的原因,以及有關修正的注釋。為了保持例子足夠的簡單,我們將關注於如何創建一個自定義特性類(DefectTrackAttribute),而該特性類僅被用於類和方法上。
DefectTrackAttribute定義的代碼如下:
using System;
namespace MyAttributeClasses
{
[AttributeUsage(AttributeTargets.Class|AttributeTargets.Method,AllowMultiple = true)]
public class DefectTrackAttribute :Attribute
{
private string cDefectID ;
private DateTime dModificationDate ;
private string cDeveloperID ;
private string cDefectOrigin ;
private string cFixComment ;
public DefectTrackAttribute ()
{
}
public DefectTrackAttribute(
string lcDefectID,
string lcModificationDate,
string lcDeveloperID )
{
this.cDefectID = lcDefectID ;
this.dModificationDate =
System.DateTime.Parse( lcModificationDate ) ;
this.cDeveloperID = lcDeveloperID ;
}
public string DefectID
{
get { return cDefectID ; }
}
public string ModificationDate
{
get
{
return dModificationDate.ToShortDateString() ;
}
}
public string DeveloperID
{
get { return cDeveloperID ; }
}
public string Origin
{
get { return cDefectOrigin ; }
set { cDefectOrigin = value ; }
}
public string FixComment
{
get { return cFixComment ; }
set { cFixComment = value ; }
}
}
}
如果你之前沒有接觸過特性,那麼你將對下面的代碼有點陌生。
這一行代碼,把特性[AttributeUsage]附加到特性類的定義上。方括號的語法表明一個特性的構造器被調用。所以,特性類也可以擁有它們自己的特性,這看起來可能有點混淆,但是隨著我給你展示可以用特性類來做些什麼,你對它的認識,將會越來越清晰。
[AttributeUsage]特性具有一個定位參數和兩個命名參數。定位參數指定了特性類將被用於何種類型,定位參數的值是枚舉AttributeTargets的組合。在我的例子裡,我僅僅把特性類應用在類和方法上,所以通過組合兩個AttributeTargets的值的滿足了我的要求。
[AttributeUsage]特性的第一個命名參數是AllowMultiple,該參數指定了是否可以在同一個類型上應用多次(你所定義的)特性類。默認值是false,即不允許應用多次。但是,根據這個例子的實際情況,你將會在某一類型上不止一次的應用特性(DefectTrackAttribute),所以應該使用[AttributeUsage]的命名參數AllowMultiple,並將其設置為true。這是因為,一個特定的類和方法在其生命周期裡會經歷多次修訂,所以你需要使用[DefectTrackAttribute]特性記錄每一次變化。
[AttributeUsage]特性的第二個命名參數是Inherited,它指定了派生類(使用此特性類的子類)是否繼承此特性。我使用了此參數的默認的值false。因為我使用的是默認值,所以也就不需要指定該命名參數。為什麼不需要繼承呢?我想獲取源代碼的修改信息是跟每一個具體的類和方法有關的。如果把Inherited設為true,那麼開發者將會混淆一個類的[DefectTrackAttribute]特性,無法辨別[DefectTrackAttribute]特性是它自己的還是從父類繼承的。
上面的代碼展示了特性類(DefectTrackAttribute)的定義。它繼承於System.Attribute,事實上,所有的特性均直接或間接的繼承於System.Attribute。
上面的代碼裡,還定義了特性的5個私有的字段,這些字段均用於保存與特性相關的值。
在我們特性類中第一個方法是構造器,它是帶有3個參數的簽名。構造器的參數對於特性類而言,就是這個特性的定位參數,這些參數是強制性的。如果你願意,你可以重載構造器,使其可以擁有更多的有關定位參數配置的選擇。
我們的特性類中剩下的部分就是一些公有屬性的聲明,這些屬性與類中的私有字段相對應。當你查閱元數據的時候,你可以使用這些屬性訪問該特性的值。需要說明的是,對應定位參數的屬性沒有set語句,只有get語句。這就導致了這些屬性是只讀的,這也與它們是定位參數而不是命名參數的含義相一致。
應用自定義特性
你現在已經知道在C#代碼裡,在一個類型聲明之前,通過在方括號裡使用特性的名字和參數就可以將其附加到目標類型上。
在下面的代碼裡,把[DefectTrack]特性附加到一對類和一對方法上。
using System ;
using MyAttributeClasses ;
namespace SomeClassesToTest
{
[DefectTrack( "1377", "12/15/02", "David Tansey" ) ]
[DefectTrack( "1363", "12/12/02", "Toni Feltman",
Origin = "Coding: Unhandled Exception" ) ]
public class SomeCustomPricingClass
{
public double GetAdjustedPrice(
double tnPrice,
double tnPctAdjust )
{ return tnPrice + ( tnPrice * tnPctAdjust ) ; }
[DefectTrack( "1351", "12/10/02", "David Tansey",
Origin = "Specification: Missing Requirement",
FixComment = "Added PriceIsValid( ) function" ) ]
public bool PriceIsValid( double tnPrice )
{ return tnPrice > 0.00 && tnPrice < 1000.00 ; }
}
[DefectTrack( "NEW", "12/12/02", "Mike Feltman" ) ]
public class AnotherCustomClass
{
string cMyMessageString ;
public AnotherCustomClass( ){ }
[DefectTrack( "1399", "12/17/02", "David Tansey",
Origin = "Analysis: Missing Requirement" ) ]
public void SetMessage( string lcMessageString )
{ this.cMyMessageString = lcMessageString ; }
}
}
首先,需要確保你可以訪問之前創建的自定義特性,所以需要添加這樣一行代碼,如下:
using MyAttributeClasses ;
到此,你就可以使用自定義特性[DefectTrack]裝飾或點綴你的類聲明和方法了。
SomeCustomPricingClass有兩處地方用到了[DefectTrack]特性。第一個[DefectTrack]特性僅僅使用了三個定位參數,而第二個[DefectTrack]特性還包含了一個命名參數Origin的指定。
[DefectTrack( "1377", "12/15/02", "David Tansey" ) ]
[DefectTrack( "1363", "12/12/02", "Toni Feltman",
Origin = "Coding: Unhandled Exception" ) ]
public class SomeCustomPricingClass
{}
PriceIsValid()方法也使用了自定義特性[DefectTrack],並且指定了兩個命名參數Origin和FixComment。上述代碼包含了[DefectTrack]特性幾個額外的用途,你可以檢測這些特性。
一些讀者可能會感到驚奇,因為對於源代碼修改的信息可以通過使用注釋這種傳統的做法已經使用工具,通過在注釋裡使用XML塊,把這些信息很好的組織起來。
在源代碼對應的位置,你可以很容易的看到你的注釋。你可以通過文本,分析源代碼裡的注釋,從而處理這些信息,但是這個過程是單調冗長的,並且很容易出現錯誤。.NET提供了工具來處理注釋裡的XML塊,這樣可以消除此類問題。
使用自定義特性可以使你達到同樣的效果,它同樣提供了一種可以有效組織的方法,用於記錄和處理這些信息,並且它還有一個額外的優勢。考慮如下情況,當把源代碼編譯成二進制代碼的時候,你是否已經丟失了代碼的注釋?毫無疑問,注釋已經作為副產品,永遠的從可執行代碼裡移出。相比之下,特性的值已經變成了元數據的一部分,永遠的綁定到一個程序集裡。在沒有源代碼的情況下,你依然可以訪問這些注釋信息。
另外,在源代碼裡允許特性構造一個與當初在設計時值一樣的實例。
獲取自定義特性的值
到此,盡管你已經在類和方法上應用了自定義屬性,但在實戰中你還沒有真正的看到它。不管你是否附加了特性,看起來好像什麼事情也沒有發生。但事實上,事情已經發生了變化,你完全不用理會我的話,你可以用MSIL反編譯工具,打開一個包含使用了自定義特性類型的EXE或者DLL文件。MSIL反編譯工具能使你看到在IL代碼裡你定義的特性和它的值。圖一是使用ILDASM工具,打開本文中例子編譯的EXE文件所看到的。
圖一:C#特性
盡管通過反編譯程序集,看到了特性的值,證明了它們的確存在,但是你仍然沒有看到跟它們相關的行為。那麼現在,你就可以使用反射API遍歷一個程序集包含的類型,查詢你自定義的特性,在應用了特性的類型上獲取特性的值。
考慮如下測試代碼的一般的做法。程序加載指定的程序集,得到一個包含程序集中所有成員的數組,在它們中間,迭代尋找應用了[DefectTrack]特性的類。對於應用了[DefectTrack]特性的類,測試程序將在控制台上輸出特性的值。對於類型中的方法,程序仍然采用了同樣的步驟和迭代。這些循環采用它們的方式在整個程序集裡“游走”。
using System ;
using System.Reflection ;
using MyAttributeClasses ;
public class TestMyAttribute
{
public static void Main( )
{
DisplayDefectTrack( "MyAttributes" ) ;
Console.ReadLine();
}
public static void DisplayDefectTrack(
string lcAssembly )
{
Assembly loAssembly =
Assembly.Load( lcAssembly ) ;
Type[ ] laTypes = loAssembly.GetTypes( ) ;
foreach( Type loType in laTypes )
{
Console.WriteLine("*======================*" ) ;
Console.WriteLine( "TYPE:\t" +
loType.ToString( ) ) ;
Console.WriteLine( "*=====================*" ) ;
object[ ] laAttributes =
loType.GetCustomAttributes(
typeof( DefectTrackAttribute ),
false ) ;
if( laAttributes.Length > 0 )
Console.WriteLine( "\nMod/Fix Log:" ) ;
foreach( Attribute loAtt in laAttributes )
{
DefectTrackAttribute loDefectTrack =
(DefectTrackAttribute)loAtt ;
Console.WriteLine( "----------------------" ) ;
Console.WriteLine( "Defect ID:\t" +
loDefectTrack.DefectID ) ;
Console.WriteLine( "Date:\t\t" +
loDefectTrack.ModificationDate ) ;
Console.WriteLine( "Developer ID:\t" +
loDefectTrack.DeveloperID ) ;
Console.WriteLine( "Origin:\t\t" +
loDefectTrack.Origin ) ;
Console.WriteLine( "Comment:\n" +
loDefectTrack.FixComment ) ;
}
MethodInfo[ ] laMethods =
loType.GetMethods(
BindingFlags.Public |
BindingFlags.Instance |
BindingFlags.DeclaredOnly ) ;
if( laMethods.Length > 0 )
{
Console.WriteLine( "\nMethods: " ) ;
Console.WriteLine( "----------------------" ) ;
}
foreach( MethodInfo loMethod in laMethods )
{
Console.WriteLine( "\n\t" +
loMethod.ToString( ) ) ;
object[ ] laMethodAttributes =
loMethod.GetCustomAttributes(
typeof( DefectTrackAttribute ),
false ) ;
if( laMethodAttributes.Length > 0 )
Console.WriteLine( "\n\t\tMod/Fix Log:" ) ;
foreach( Attribute loAtt in laMethodAttributes )
{
DefectTrackAttribute loDefectTrack =
(DefectTrackAttribute)loAtt ;
Console.WriteLine( "\t\t----------------" ) ;
Console.WriteLine( "\t\tDefect ID:\t" +
loDefectTrack.DefectID ) ;
Console.WriteLine( "\t\tDeveloper ID:\t" +
loDefectTrack.DeveloperID ) ;
Console.WriteLine( "\t\tOrigin:\t\t" +
loDefectTrack.Origin ) ;
Console.WriteLine( "\t\tComment:\n\t\t" +
loDefectTrack.FixComment ) ;
}
}
Console.WriteLine( "\n\n" ) ;
}
}
}
讓我們來看一下比較重要的幾行代碼。DisplayDefectTrack()方法的第一行代碼和第二行代碼得到了加載指定程序集的一個引用並且得到了包含在該程序集中類型的一個數組。
Assembly.Load( lcAssembly ) ;
Type[ ] laTypes = loAssembly.GetTypes( ) ;
使用foreach語句在程序集中的每一個類型上迭代。在控制台上輸出當前類型的名稱,並使用如下的語句查詢當前類型,獲取有關[DefectTrack]特性的一個數組。
object[ ] laAttributes =
loType.GetCustomAttributes(
typeof( DefectTrackAttribute ),
false ) ;
你需要在GetCustomAttributes方法上指定typeof(DefectTrackAttribute) 參數,以限制僅僅返回你創建的自定義特性。第二個參數false指定是否搜索該成員的繼承鏈以查找這些自定義特性。
使用foreach語句迭代自定義特性數組,並把它們(自定義特性)的值輸出到控制台上。你應該認識到第一個foreach語句塊會創建一個新的變量,並且對當前的特性作類型轉化。
DefectTrackAttribute loDefectTrack =
(DefectTrackAttribute)loAtt ;
這一條語句為什麼是必須的呢?GetCustomAttributes()方法會返回一個object數組,你為了訪問自定義特性的值,所以必須把這些引用轉化為它們真正的具體類的引用。轉化完以後,你就可以使用這些特性並且可以把特性的值輸出到控制台上。
因為你可以在任意的類和方法上應用特性,因此程序需要調用當前類型上的方法GetMethods()。
MethodInfo[ ] laMethods =
loType.GetMethods(
BindingFlags.Public |
BindingFlags.Instance |
BindingFlags.DeclaredOnly ) ;
在這個例子裡,我給GetMethods()方法傳遞了一些BindingFlags枚舉值。組合使用這三個枚舉值,限制僅僅返回在當前的類中直接定義的方法。在這個例子裡,之所以這樣做,是因為我想限制輸出的數量,但是在實際當中,你可能並不需要這樣做,因為開發人員可能會在一個重寫的方法上應用[DefectTrack]特性。而我的實現代碼並沒有捕捉應用在這些方法上的特性。
剩下的代碼,從本質上來說,對每一個方法以及每一個類,都在做相同的操作。都是在每一個方法上尋找是否應用了[DefectTrack]特性,如果應用了,就把特性的值輸出到控制台上。
總結
在這裡,我只是利用一個簡單的例子,介紹了開發者如何使用.NET特性提高開發進程。自定義特性有點類似於XML,它最大的好處不在於“它做了什麼”,它真正最大的好處在於“你可以用它做什麼”。這個是真正無止境的,由於自定義特性本身具有開放的特性,這使得它可以擁有更多新穎的用途。
From:http://tw.wingwit.com/Article/program/net/201311/12373.html