Attribute的基本概念
經常有周邊人問,Attribute是什麼?它有什麼用?好像沒有這個東東程序也能運行。實際上在.Net中,Attribute是一個非常重要的組成部分,本文整理相關資料,提供給大家參考。
首先,我們肯定Attribute是一個類,下面是msdn文檔對它的描述:公共語言運行時允許你添加類似關鍵字的描述聲明,叫做attributes, 它對程序中的元素進行標注,如類型、字段、方法和屬性等。
Attributes和Microsoft .NET Framework文件的元數據保存在一起,可以用來向運行時描述你的代碼,或者在程序運行的時候影響應用程序的行為。
在.NET中,Attribute被用來處理多種問題,比如序列化、程序的安全特征、防止即時編譯器對程序代碼進行優化從而代碼容易調試等等。
復雜的,面向組件的業務開發,期待現代的軟件開發工程師們具備更多的彈性設計,而不是過去的方法設計。微軟的.NET框架通過眾所周知的聲明式編程,廣泛的使用特性來附加額外的功能。在軟件系統裡,使用特性可以增強系統的彈性,這是因為,特性使功能的低耦合得到了增強。所以,你可以定制自己的特性類,然後根據你自己的意圖,合理的使用這些具有低耦合功效的特性。
使用.NET框架編寫Windows程序,在很多方面已經變得很簡單。在許多情況下,.NET框架使用.NET編譯器,在編譯時綁定到程序集的元數據,使靈活的程序設計變得更容易。事實上,對於.NET而言,使用內嵌的元數據把我們從DLL地獄解脫出來是可能的。
值得慶幸的是,.NET框架的設計者們並沒有選擇把這些元數據優雅的隱藏起來。設計者們把反射API給予了我們,通過反射,一個.NET程序可以通過編程查看這個元數據。一個程序可以反射出包含在特定程序集內任意的東西,或者說是包含在其內的所有的類型和成員。(反射內容待續,請關注)
把元數據綁定到可執行的程序集裡,提供了許多優勢。這使得.NET程序集,完全可以自我描述。還允許開發者跨語言共享組件,去除了頭文件的需要。(這些頭文件會由於相關的實現代碼而過期。)
關於.NET元數據所有好的地方,看起來很難讓人相信,它好像沒什麼用,僅僅是個謊言。但是,它確實是存在的。在.NET裡,你可以創建自己特定程序的元數據,並且可以把這些元數據應用到你可以想象到的地方。
開發者通過使用自定義特性,可以定義他們自己特定程序的元數據。因為這些特性的值將變成另一部分元數據,綁定到一個程序集裡。所以這些自定義特性的值可以被反射API檢查到並且可以被使用。
問題:我們經常提到一個類的屬性,這些屬性的值可以作為特性來使用。那麼屬性和自定義特性真正的區別在哪裡呢?
通過這篇文章,你將學會如何定制特性,如何把特性應用到你的源代碼類和方法上,以及如何使用反射API獲取和使用這些特性的值。
公共語言運行時是如何使用特性的?
在你開始考慮如何使用你自己定義的特性類之前,讓我們查看一些標准的特性,這些已經在公共語言運行時有用到。
[WebMethod]特性提供了一個簡單的例子。它可以使WebService派生的子類中任意公共的方法轉化成Web Service暴露方法的一部分,而這一切,僅僅通過把[WebMethod]附加到方法的定義上就可以做到。
public class SomeWebService : System.Web.Services.WebService
{
[WebMethod]
public DataSet GetDailySales()
{
//處理請求的代碼
}
}
你只要把[WebMethod]特性添加到一個方法上,.NET就會在後台為你處理其它所有的事情。
在給定的方法上使用[Conditional]特性,那麼此方法是否可調用將取決於指定的預處理標識符是否被定義。舉個例子,看如下的代碼:
public class SomeClass
{
[Conditional("DEBUG")]
public void UnitTest()
{
//單元測試代碼
}
}
這段代碼說明,該類的方法UnitTest()是否有效,將取決於預處理標識符“DEBUG”是否被定義(譯注:在編譯調試版本時,DEBUG常數已經被定義)。我們可能更感興趣的是,使用[Conditional]後真正發生了什麼。當條件失效時,編譯器將會停止所有對該方法的調用,相比有同樣功能的預處理指令#if...#endif,此方法顯得更簡潔,而且,使用這項功能,我們不需要多做任何事情。
特性使用了定位參數和命名參數。在使用了[Conditional]特性的例子中,特定的符號就是定位參數。定位參數是強制性的,你必須提供。
讓我們回到使用了[WebMethod]特性的例子,來看一下命名參數。這個特性有一個Description的命名參數,可以像下面這樣使用:
[WebMethod(Description = "Sales volume")]
命名參數是可選擇的,參數的值要緊跟著寫在參數名字的後面。如果存在定位參數,那麼你需要先書寫定位參數,然後在定位參數的後面書寫命名參數。
我將會在文章的後面講述更多關於定位參數和命名參數的內容,這將在我向你展示如何創建和使用你自己的特性類時提到。
特性可用於運行時,設計時
在這篇文章裡,我提供的都是與運行時的行為相關的例子。但是二進制文件(程序集)並不只是用於運行時。在.NET裡,你所定義的元數據也不只是局限於運行時,相反,當你編譯成程序集後,在任何時候你都可以查閱這些元數據。
考慮在設計時,使用元數據的一些可能的情況。在Visual Studio.Net裡,使用IDE可以構建工具(使用.NET語言),方便開發和設計(向導,構建器等等)。這樣,一個模塊的運行時的環境(如:IDE工具)就成了另一個模塊的設計時環境(被開發的源代碼)。這裡提供了一個使用定制特性很好的例子。IDE工具將會反射你編寫的類和類型,然後遵照你的代碼行事。不幸的是,由於沒有IDE工具的代碼,探究這樣的例子,已經超出了該文章所闡述的范圍。
標准的.NET特性包含了一個類似的例子。當一個開發者創建自定義控件並把它放到Visual Studio.Net IDE的工具箱中,它們(自定義控件)已經使用了一系列特性,用於說明在屬性表單中如何處理自定義控件。下面列舉並描述了在屬性表單中用到的4種標准的.NET特性。
在Visual Studio .NET IDE裡設計時屬性表單用到的標准的.NET特性:
Attribute Description
Designer 指定用於為組件實現設計時服務的類。
DefaultProperty 指定在屬性表單中,組件的默認的屬性。
Category 指定在屬性表單中,屬性的類別。
Description 指定在屬性表單中,有關屬性的描述。
這些與表單相關的特性,讓我們認識到,可以在設計時使用特性以及它們的值,就像在運行時一樣。
自定義特性類的屬性
在特性和類的屬性之間存在著明顯相似的地方。這給我們何時、何處應該使用自定義特性帶來了困惑。開發者們通常引用一個類的屬性,並把屬性的值作為自己“特性”,那麼屬性和特性之間真正的區別在哪裡呢?
當你定義特性的時候,它和屬性沒有根本的區別,使用時,可以以相同的方式把它附加到程序集不同的類型上,而不僅僅在類上使用。下面列舉了可以應用特性的所有程序集類型。
可以應用特性的所有程序集類型。
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(AttributeTargets.Class|AttributeTargets.Method,AllowMultiple =true)]
這一行代碼,把特性[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/2007", "David Tansey" ) ]
[DefectTrack( "1363", "12/12/2007", "Toni Feltman",
Origin = "Coding: Unhandled Exception" ) ]
public class SomeCustomPricingClass
{
public double GetAdjustedPrice( double tnPrice, double tnPctAdjust )
{
return tnPrice + ( tnPrice * tnPctAdjust ) ;
}
[DefectTrack( "1351", "12/10/2007", "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/2007", "Mike Feltman" ) ]
public class AnotherCustomClass
{
string cMyMessageString ;
public AnotherCustomClass( ) { }
[DefectTrack( "1399", "12/17/2007", "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/2007", "David Tansey" ) ]
[DefectTrack( "1363", "12/12/2007", "Toni Feltman",
Origin = "Coding: Unhandled Exception" ) ]
public class SomeCustomPricingClass
{ }
PriceIsValid()方法也使用了自定義特性[DefectTrack],並且指定了兩個命名參數Origin和FixComment。上述代碼包含了[DefectTrack]特性幾個額外的用途,你可以檢測這些特性。
一些讀者可能會感到驚奇,因為對於源代碼修改的信息可以通過使用注釋這種傳統的做法。.NET已經使用工具,通過在注釋裡使用XML塊,把這些信息很好的組織起來。
使用自定義特性可以使你達到同樣的效果,它同樣提供了一種可以有效組織的方法,用於記錄和處理這些信息,並且它還有一個額外的優勢。考慮如下情況,當把源代碼編譯成二進制代碼的時候,你是否已經丟失了代碼的注釋?毫無疑問,注釋已經作為副產品,永遠的從可執行代碼裡移出。相比之下,特性的值已經變成了元數據的一部分,永遠的綁定到一個程序集裡。在沒有源代碼的情況下,你依然可以訪問這些注釋信息。另外,在源代碼裡允許特性構造一個與當初在設計時值一樣的實例。
獲取自定義特性的值
到此,盡管你已經在類和方法上應用了自定義屬性,但在實戰中你還沒有真正的看到它。不管你是否附加了特性,看起來好像什麼事情也沒有發生。但事實上,事情已經發生了變化,你完全不用理會我的話,你可以用MSIL反編譯工具,打開一個包含使用了自定義特性類型的EXE或者DLL文件。MSIL反編譯工具能使你看到在IL代碼裡你定義的特性和它的值。
盡管通過反編譯程序集,看到了特性的值,證明了它們的確存在,但是你仍然沒有看到跟它們相關的行為。那麼現在,你就可以使用反射API遍歷一個程序集包含的類型,查詢你自定義的特性,在應用了特性的類型上獲取特性的值。
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 loAssembly = 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,它最大的好處不在於“它做了什麼”,它真正最大的好處在於“你可以用它做什麼”。這個是真正無止境的,由於自定義特性本身具有開放的特性,這使得它可以擁有更多新穎的用途。
摘自 愛上了月兒就注定要為她擋太陽