Note程序員
- 類的元數據包含該類的成員和特性
- 程序的元數據能夠理解爲程序的結構信息
- 反射(reflection)用來查看元數據
- C#中經過Type類來反射
- 特性(attribute)用來給類型添加元數據
PS:理解有待增強數組
大多數程序都要處理數據,包括讀、寫、操做和顯示數據。(圖形也是一種數據的形式。)然而,對於某些程序來講,它們操做的數據不是數字、文本或圖形,而是程序和程序類型自己的信息。瀏覽器
對象瀏覽器是顯式元數據的程序的一個示例。它能夠讀取程序集,而後顯示所包含的類型以及類型的全部特性和成員。
本章將介紹程序如何使用Type類來反射數據,以及程序員如何使用特性來給類型添加元數據。安全
要使用反射,咱們必須使用System.Reflection命名空間。app
以前已經介紹瞭如何聲明和使用C#中的類型。包括預約義類型(int、long和string等)、BCL中的類型(Console、IEnumerable等)以及用戶自定義類型(MyClass、Mydel等)。每一種類型都有本身的成員和特性。
BCL聲明瞭一個叫作Type的抽象類,它被設計用來包含類型的特性。使用這個類的對象能讓咱們獲取程序使用的類型的信息。
因爲Type是抽象類,所以它不能有實例。而是在運行時,CLR建立從Type(RuntimeType)派生的類的實例,Type包含了類型信息。當咱們要訪問這些實例時,CLR不會返回派生類的引用而是Type基類的引用。可是,爲了簡單起見,在本章剩餘的篇幅中,我會把引用所指向的對象稱爲Type類型的對象(雖然從技術角度來講是一個BCL內部的派生類型的對象)。
須要瞭解的有關Type的重要事項以下:框架
下圖顯示了一個運行的程序,它有兩個MyClass對象和一個OtherClass對象。注意,儘管有兩個MyClass的實例,只會有一個Type對象來表示它。
咱們能夠從Type對象中獲取須要瞭解的有關類型的幾乎全部信息。下表列出了類中更有用的成員。
函數
本節學習使用GetType方法和typeof運算符來獲取Type對象。object類型包含了一個叫作GetType的方法,它返回對實例的Type對象的引用。因爲每個類型最終都是從object繼承的,因此咱們能夠在任何類型對象上使用GetType方法來獲取它的Type對象,以下所示: 學習
Type t = myInstance.GetType();
下面的代碼演示瞭如何聲明一個基類以及從它派生的子類。Main方法建立了每個類的實例而且把這些引用放在了一個叫作bca的數組中以方便使用。在外層的foreach循環中,代碼獲得了Type對象而且輸出類的名字,而後獲取類的字段並輸出。下圖演示了內存中的對象。
spa
using System; using System.Reflection; class BaseClass { public int BaseField=0; } class DerivedClass:BaseClass { public int DerivedField=0; } class Program { static void Main() { var bc=new BaseClass(); var dc=new DerivedClass(); BaseClass[] bca=new BaseClass[]{bc,dc}; foreach(var v in bca) { Type t=v.GetType(); Console.WriteLine("Object type : {0}",t.Name); FieldInfo[] fi=t.GetFields(); foreach(var f in fi) { Console.WriteLine(" Field : {0}",f.Name); } Console.WriteLine(); } } }
咱們還可使用typeof運算符來獲取Type對象。只須要提供類型名做爲操做數,它就會返回Type對象的引用,以下所示:設計
Type t = typeof(DerivedClass); ↑ ↑ 運算符 但願的Type對象的類型
下面的代碼給出了一個使用typeof運算符的簡單示例:
using System; using System.Reflection; namespace SimpleReflection { class BaseClass { public int MyFieldBase; } class DerivedClass:BaseClass { public int MyFieldDerived; } class Program { static void Main() { Type tbc=typeof(DerivedClass); Console.WriteLine("Result is {0}.",tbc.Name); Console.WriteLine("It has the following fields:"); FieldInfo[] fi=tbc.GetFields(); foreach(var f in fi) { Console.WriteLine(" {0}",f.Name); } } } }
特性(attribute)是一種容許咱們向程序的程序集增長元數據的語言結構。它是用於保存程序結構信息的某種特殊類型的類。
下圖是使用特性中相關組件的概覽,而且也演示了以下有關特性的要點。
根據慣例,特性名使用Pascal命名法而且以Attribute後綴結尾。當爲目標應用特性時,咱們能夠不使用後綴。例如,對於SerializableAttribute和MyAttributeAttribute這兩個特性,咱們在把它們應用到結構時可使用Serializable和MyAttribute短名稱。
咱們先不講解如何建立特性,而是看看如何使用已定義的特性。這樣,你會對它們的使用狀況有個大體瞭解。
特性的目的是告訴編譯器把程序結構的某組元數據嵌入程序集。咱們能夠經過把特性應用到結構來實現。
例如,下面的代碼演示了兩個類的開始部分。最初的幾行代碼演示了把一個叫作Serializable的特性應用到MyClass。注意,Serializable沒有參數列表。第二個類的聲明有一個叫作MyAttribute的特性,它有一個帶有兩個string參數的參數列表。
[Serializable] public class MyClass { … } [MyAttribute("Simple class","Version 3.57")] public class MyOtherClass { … }
有關特性須要瞭解的重要事項以下:
在學習如何定義本身的特性以前,本小節會先介紹幾個.NET預約義特性。
一個程序可能在其生命週期中經歷屢次發佈,並且極可能延續多年。在程序生命週期的後半部分,程序員常常須要編寫相似功能的新方法替換老方法。出於多種緣由,你可能不想再使用那些調用過期的舊方法的老代碼,而只想用新編寫的代碼調用新方法。
若是出現這種狀況,你確定但願稍後操做代碼的團隊成員或程序員也只使用新代碼。要警告他們不要使用舊方法,可使用Obsolete特性將程序結構標註爲過時的,而且在代碼編譯時顯式有用的警告消息。如下代碼給出了一個使用的示例:
class Program { //應用特性 [Obsolete("User method SuperPrintOut")] static void PrintOut(string str) { Console.WriteLine(str); } static void Main(string[] args) { PrintOut("Start of Main"); } }
注意,即便PrintOut被標註爲過時,Main方法仍是調用了它。代碼編譯也運行得很好而且產生了以下的輸出:
不過,在編譯的過程當中,編譯器產生了下面的CS0618警告消息來通知咱們正在使用一個過時的結構:
另一個Obsolete特性的重載接受了bool類型的第二個參數。這個參數指定目標是否應該被標記爲錯誤而不只僅是瞥告。如下代碼指定了它須要被標記爲錯誤:
標記爲錯誤 ↓ [Obsolete("User method SuperPrintOut",true)] static void PrintOut(string str) { … }
Note
Conditional特性相似於C語言的條件編譯
Conditional特性容許咱們包括或排斥特定方法的全部調用。爲方法聲明應用Conditional特性並把編譯符做爲參數來使用。
定義方法的CIL代碼自己老是會包含在程序集中。只是調用代碼會被插入或忽略。
例如,在以下的代碼中,把Conditional特性應用到對一個叫作TraceMessage的方法的聲明上。特性只有一個參數,在這裏是字符串DoTrace。
[Conditional("DoTrace")] static void TraceMessage(string str) { Console.WriteLine(str); }
Conditional特性的示例
如下代碼演示了一個使用Conditional特性的完整示例。
#define DoTrace using System; using System.Diagnostics; namespace AttributeConditional { class Program { [Conditional("DoTrace")] static void TraceMessage(string str) { Console.WriteLine(str); } static void Main() { TraceMessage("Start of Main"); Console.WriteLine("Doing work in Main."); TraceMessage("End of Main"); } } }
若是註釋掉第一行來取消DoTrace的定義,編譯器就再也不會插人兩次對TraceMessage的調用代碼。此次,若是咱們運行程序,就會產生以下輸出:
調用者信息特性能夠訪問文件路徑、代碼行數、調用成員的名稱等源代碼信息。
CallerFilePath
、CallerLineNumber
和CallerMemberName
下面的代碼聲明瞭一個名爲MyTrace的方法,它在三個可選參數上使用了這三個調用者信息特性。若是調用方法時顯式指定了這些參數,則會使用真正的參數值。但在下面所示的Main方法中調用時,沒有顯式提供這些值,所以系統將會提供源代碼的文件路徑、調用該方法的代碼行數和調用該方法的成員名稱。
using System; using System.Runtime.CompilerServices; public static class Program { public static void MyTrace(string message, [CallerFilePath] string fileName="", [CallerLineNumber] int lineNumber=0, [CallerMemberName] string callingMember="") { Console.WriteLine("File: {0}",fileName); Console.WriteLine("Line: {0}",lineNumber); Console.WriteLine("Called From: {0}",callingMember); Console.WriteLine("Message: {0}",message); } public static void Main() { MyTrace("Simple message"); } }
咱們在單步調試代碼時,經常但願調試器不要進入某些方法。咱們只想執行該方法,而後繼續調試下一行。DebuggerStepThrough特性告訴調試器在執行目標代碼時不要進入該方法調試。
在我本身的代碼中,這是最常使用的特性。有些方法很小而且毫無疑問是正確的,在調試時對其反覆單步調試只能徒增煩惱。但使用該特性時要十分當心,由於你並不想排除那些可能含有bug的代碼。
關於DebuggerStepThrough要注意如下兩點:
下面這段隨手編造的代碼在一個訪問器和一個方法上使用了該特性。你會發現,調試器調試這段代碼時不會進入IncrementFields方法或X屬性的set訪問器。
using System; using System.Diagnostics; class Program { int _x=1; int X { get{return _x;} [DebuggerStepThrough] set { _x=_x*2; _x+=value; } } public int Y{get;set;} public static void Main() { var p=new Program(); p.IncrementFields(); p.X=5; Console.WriteLine("X = {0}, Y = {1}",p.X,p.Y); } [DebuggerStepThrough] void IncrementFields() { X++; Y++; } }
.NET框架預約義了不少編譯器和CLR能理解和解釋的特性,下表列出了一些。在表中使用了不帶Attribute後綴的短名稱。例如,CLSCompliant的全名是CLSCompliantAttribute。
至此,咱們演示了特性的簡單使用,都是爲方法應用單個特性。這部份內容將會講述其餘特性的使用方式。
咱們能夠爲單個結構應用多個特性。
例如,下面的兩個代碼片斷顯示了應用多個特性的兩種方式。兩個片斷的代碼是等價的。
[Serializable ] //多層結構 [MyAttribute("Simple class", "Version 3.57")] [MyAttribute("Simple class", "Version 3.57"),Serializable] //逗號分隔
除了類,咱們還能夠將特性應用到諸如字段和屬性等其餘程序結構。如下的聲明顯示了字段上的特性以及方法上的多個特性:
[MyAttribute("Holds a value", "Version 3.2")] //字段上的特性 public int MyField; [Obsolete] //方法上的特性 [MyAttribute("Prints out a message.", "Version 3.6")] public void Printout() { … }
咱們還能夠顯式地標註特性,從而將它應用到特殊的目標結構。要使用顯式目標,在特性片斷的開始處放置目標類型,後面跟冒號。例如,以下的代碼用特性裝飾方法,而且還把特性應用到返回值上。
顯式目標說明符 ↓ [method: MyAttribute("Prints out a message.", "Version 3.6")] [return: MyAttribute("This value represents …", "Version 2.3")] public long ReturnSetting() { … }
以下表所列,C#語言定義了10個標準的特性目標。大多數目標名能夠自明(self-explanatory),而type覆蓋了類、結構、委託、枚舉和接口。 typevar目標名稱指定使用泛型結構的類型參數。
咱們還能夠經過使用assembly和module目標名稱來使用顯式目標說明符把特性設置在程序集或模塊級別。(程序集和模塊在第21章中解釋過。)一些有關程序集級別的特性的要點以下:
以下的代碼行摘自AssemblyInfo.cs文件:
[assembly: AssemblyTitle("SuperWidget")] [assembly: AssemblyDescription("Implements the SuperWidget product.")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("McArthur Widgets, Inc.")] [assembly: AssemblyProduct("Super Widget Deluxe")] [assembly: AssemblyCopyright("Copyright © McArthur Widgets 2012")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")]
你或許已經注意到了,應用特性的語法和以前見過的其餘語法很不相同。你可能會以爲特性是和結構徹底不一樣的類型,其實不是,特性只是某個特殊類型的類。
有關特性類的一些要點以下。
整體來講,聲明一個特性類和聲明其餘類同樣。然而,有一些事項值得注意,以下所示。
例如,下面的代碼顯示了MyAttributeAttribute特性的聲明的開始部分:
特性名 基類 ↓ ↓ public sealed class MyAttributeAttribute : System.Attribute { … }
因爲特性持有目標的信息,全部特性類的公共成員只能是:
特性和其餘類同樣,都有構造函數。每個特性至少必須有一個公共構造函數。
例如,若是有以下的構造函數(名字沒有包含後綴),編譯器會產生一個錯誤消息:
public MyAttributeAttribute(string desc,string ver) { Description=desc; VersionNumber=ver; }
當咱們爲目標應用特性時,實際上是在指定應該使用哪一個構造函數來建立特性的實例。列在特性應用中的參數其實就是構造函數的參數。
例如,在下面的代碼中,MyAttribute被應用到一個字段和一個方法上。對於字段,聲明指定了使用單個字符串的構造函數。對於方法,聲明指定了使用兩個字符串的構造函數。
[MyAttribute("Holds a value")] //使用一個字符串的構造函數 public int MyField; [MyAttribute("version 1.3", "Sal Martin")] //使用兩個字符串的構造函數 public void MyMethod() { … }
其餘有關特性構造函數的要點以下。
[MyAttr] class SomeClass … [MyAttr()] class OtherClass …
和其餘類同樣,咱們不能顯式調用構造函數。特性的實例建立後,只有特性的消費者訪問特性時才能調用構造函數。這一點與其餘類的實例很不相同,這些實例都建立在使用對象建立表達式的位置。應用一個特性是一條聲明語句,它不會決定何時構造特性類的對象。
下圖比較了普通類構造函數的使用和特性的構造函數的使用。
和普通類的方法與構造方法類似,特性的構造方法一樣可使用位置參數和命名參數。以下代碼顯示了使用一個位置參數和兩個命名參數來應用一個特性:
位置參數 命名參數 命名參數 ↓ ↓ ↓ [MyAttribute("An excellent class",Reviewer="Amy McArthur",Ver="0.7.15.33")]
下面的代碼演示了特性類的聲明以及爲MyClass類應用特性。注意,構造函數的聲明只列出了一個形參,但咱們可經過命名參數給構造函數3個實參。兩個命名參數設置了字段Ver和Reviewer的值。
public sealed class MyAttributeAttribute : System.Attribute { public string Description; public string Ver; public string Reviewer; public MyAttributeAttribute(string desc) //一個形參 { Description = desc; } } //三個實參 [MyAttribute("An excellent class」, Reviewer="Amy McArthur", Ver="7.15.33")] class MyClass { … }
構造函教須要的任何位置參數都必須放在命名參數以前。
咱們已經看到了能夠爲類應用特性。而特性自己就是類,有一個很重要的預約義特性能夠用來應用到自定義特性上,那就是AttributeUsage特性。咱們可使用它來限制特性使用在某個目標類型上。
例如,若是咱們但願自定義特性MyAttribute只能應用到方法上,那麼能夠以以下形式使用AttributeUsage:
只針對方法 ↓ [AttributeUsage( AttributeTarget.Method )] public sealed class MyAttributeAttribute : System.Attribute { … }
AttributeUsage有三個重要的公共屬性,以下表所示。表中顯示了屬性名和屬性的含義。對於後兩個屬性,還顯示了它們的默認值。
AttributeUsage的構造函數
AttributeUsage的構造函數接受單個位置參數,該參數指定了特性容許的目標類型。它用這個參數來設置ValidOn屬件,可接受目標類型是AttributeTarget枚舉的成員。AttributeTarget枚舉的完整成員列表以下表所示。
咱們能夠經過使用按位或運算符來組合使用類型。例如,在下面的代碼中,被裝飾的特性只能應用到方法和構造函數上。
目標 ↓ [AttributeUsage( AttributeTarget.Method| AttributeTarget.Constructor )] public sealed class MyAttributeAttribute : System.Attribute
當咱們爲特性聲明應用AttributeUsage時,構造函數至少須要一個參數,參數包含的目標類型會保存在ValidOn中。咱們還能夠經過使用命名參數有選擇性地設置Inherited和AllowMultiple屬性。若是咱們不設置,它們會保持如表24-4所示的默認值。
做爲示例,下面一段代碼指定了MyAttribute的以下方面。
[AttributeUsage( AttributeTarget.Class, //必需的,位置參數 Inherited = false, //可選的,命名參數 AllowMultiple = false )] //可選的,命名參數 public sealed class MyAttributeAttribute : System.Attribute { … }
強烈推薦編寫自定義特性時參考以下實踐。
以下代碼演示了這些準則:
[AttributeUsage( AttributeTargets.Class )] public sealed class ReviewCommentAttribute : System.Attribute { public string Description {get;set;} public string VersionNumber {get;set;} public string ReviewerID {get;set;} public ReviewCommentAttribute(string desc, string ver) { Description = desc; VersionNumber = ver; } }
在本章開始處,咱們已經看到了可使用Type對象來獲取類型信息。對於訪問自定義特性來講,咱們也能夠這麼作。Type的兩個方法(IsDefined和GetCustomAttributes)在這裏很是有用。
咱們可使用Type對象的IsDefined方法來檢測某個特性是否應用到了某個類上。
例如,如下的代碼聲明瞭一個有特性的類MyClass,而且做爲本身特性的消費者在程序中訪問聲明和被應用的特性。代碼的開始處是MyAttribute特性和應用特性的MyClass類的聲明。這段代碼作了下面的事情。
[AttributeUsage(AttributeTargets.Class)] public sealed class ReviewCommentAttribute:System.Attribute {…} [ReviewComment("Check it out","2.4")] class MyClass{} class Program { static void Main() { var mc=new MyClass(); Type t=mc.GetType(); bool isDefined= t.IsDefined(typeof(ReviewCommentAttribute),false); if(isDefined) Console.WriteLine("ReviewComment is applied to type {0}",t.Name); } }
GetCustomAttributes方法返回應用到結構的特性的數組。
object[] AttArr = t.GetCustomAttributes(false);
下面的代碼使用了前面的示例中相同的特性和類聲明。可是,在這種狀況下,它不檢測特性是否應用到了類,而是獲取應用到類的特性的數組,而後遍歷它們,輸出它們的成員的值。
using System; [AttributeUsage(AttributeTargets.Class)] public sealed class MyAttributeAttribute:System.Attribute { public string Description {get;set;} public string VersionNumber{get;set;} public string ReviewerID {get;set;} public MyAttributeAttribute(string desc,string ver) { Description=desc; VersionNumber=ver; } } [MyAttribute("Check it out","2.4")] class MyClass { } class Program { static void Main() { Type t=typeof(MyClass); object[] AttArr=t.GetCustomAttributes(false); foreach(Attribute a in AttArr) { var attr=a as MyAttributeAttribute; if(null!=attr) { Console.WriteLine("Description :{0}",attr.Description); Console.WriteLine("Version Number :{0}",attr.VersionNumber); Console.WriteLine("Reviewer ID :{0}",attr.ReviewerID); } } } }