1.什麼是Attribute
特性簡單點理解就是為目標元素添加一些附加信息,這些附加信息我們可以在運行期間以反射的方式拿到。目標元素指的是程序集、模塊、類、參數、屬性等元素,附加信息指的是特性類中的成員。可以看出特性類其實就是一個數據結構,我們可以將各種各樣的信息放入這個類中,並將特性類關聯到指定目標元素中,在目標元素中每關聯一個特性就創建一個特性類的實例,當然它的作用還不止如此。下面是使用特性的3段代碼,分別是3個類。第一個是MyAttribute特性類,第二個是與特性類關聯的目標類MyClass,第3個類是主程序類Program。在第一個類中,我使用了AttributeUsage系統特性類,.net還為我們提供了很多固定特性類,比如Serializable、Conditional、DllImport、Flags等。AttributeUsage類的參數作用我代碼裡已經有了注釋,可以添加這個特性也可以不添加,但這個特性類在我這個程序中必須添加。原因是我在第二個類MyClass中添加了4個MyAttribute特性實例,而默認情況下只允許添加一個實例。因此我得在AttributeUsage中指定AllowMultiple為true,如果不指定編譯會報錯。
在AttributeUsage特性類的參數中,並不是完全以傳值的形式創建的實例,其中還可以直接給參數賦值比如AllowMultiple和Inherited。對於前者一般是構造函數中的參數,我們必須給構造函數賦值否則就不能初始化了,因此這種類型的參數是固定參數。對於後者則是可選參數,細心點會發現它就是特性類中的屬性,比如在第二個類MyClass中就給Hobby屬性賦值了。正因為參數裡有可選參數,故MyClass可以同時關聯4個特性實例。在第三個類中,我通過GetCustomAttribute拿到定制的特性數組,其實就是MyAttribute的實例,這樣就可以通過實例對象獲取裡面的數據成員了。這便是Attribute的基本使用,由於使用時需要調用構造函數,因此定制特性類必須有公共構造函數。將程序的exe文件放入Reflector中可以很清楚的看到特性就是一個類,使用特性其實就是調用特性的構造函數。
//特性也可以用在特性上 [AttributeUsage( AttributeTargets.All, //目標元素可以是任何元素 AllowMultiple=true, //多個特性可以加在一個元素上,為false時一個元素上只允許有這個特性的唯一實例 Inherited=true)] //特性可被子類繼承,為false時不可以被繼承 class MyAttribute : Attribute { //字段 public string name="默認名字"; public int age = 0; string hobby="默認愛好"; //屬性 public string Hobby { get { return hobby; } set { hobby = value; } } //構造方法 public MyAttribute() { } public MyAttribute(string name, int age) { this.name = name; this.age = age; } //實例方法 public void haha() { Console.WriteLine("哈哈"); } }View Code
[My()] [My(Hobby="足球")] [My("小方",20)] [My("小白",30,Hobby="籃球")] class MyClass { public void haha() { Console.WriteLine("我是MyClass類"); } }View Code
class Program { static void Main(string[] args) { /* * 本來想看一下會不會默認拿第一個實例,結果執行時報錯:找到同一個類型的多個實例 //拿到MyClass上的一個特性實例 MyAttribute myAttribute = (MyAttribute)Attribute.GetCustomAttribute(typeof(MyClass), typeof(MyAttribute)); //看看這個實例是哪一個 if (myAttribute!=null) { Console.WriteLine(myAttribute.name); Console.WriteLine(myAttribute.age); Console.WriteLine(myAttribute.Hobby); myAttribute.haha(); } */ //拿到MyClass上的特性實例數組,這裡有4個MyAttribute的實例 MyAttribute[] myAttributes = (MyAttribute[])Attribute.GetCustomAttributes(typeof(MyClass), typeof(MyAttribute)); MyAttribute myAttribute = myAttributes[0]; if (myAttribute != null) { Console.WriteLine(myAttribute.name); Console.WriteLine(myAttribute.age); Console.WriteLine(myAttribute.Hobby); myAttribute.haha(); } Console.ReadLine(); } } /*執行結果: 小白 30 籃球 哈哈 */View Code
2.Attribute的作用
因為特性的存在,讓我們可以在程序運行時得到一些信息,再根據這些信息進行邏輯判斷。比如可以使用特性來確保Model對象的數據全部去除空字符串,代碼如下面所示。第一段代碼指特性類MyAttribute,第二段代碼指使用特性的MyClass類,第三段代碼指MyClass類的擴展方法Trim,第四段代碼指主程序類Program。在main方法中創建了一個myclass對象,在給這個對象賦值時特意指定了一些空格。假設現在需要將MyClass這個類作為數據庫實體類People,它的實例存放著輸入的數據,這個數據可能有空格。一般情況下我得調用trim()方法來去除空格,但是如果MyClass的屬性很多的話那就很麻煩了,需要寫很多ToString().Trim()方法。而使用特性+擴展方法則可以輕松很多,對於需要進行空格去除的屬性添加一個MyAttribute特性,接著調用實例對象的Trim擴展方法。在Trim方法中,會遍歷這個對象的所有屬性,接著遍歷每個屬性的所有特性,並找到打了MyAttribute特性的屬性,接著進行ToString().Trim()方法的調用並重新給屬性賦值,這樣只需寫一句myclass.Trim()就可以實現除掉空格的功能。如果沒有特性,雖然一樣可以使用擴展方法來對屬性進行去除空格,但是我們無法對指定的屬性進行去除,只能一口氣把所有類型為string的字符串空格全都去除。
[AttributeUsage(AttributeTargets.Property,Inherited=false,AllowMultiple=false)] public class TrimAttribute : Attribute { //字段與屬性 readonly Type myType; public Type MyType { get { return this.myType; } } //構造函數 public TrimAttribute(Type type) { myType = type; } }View Code
class MyClass { [TrimAttribute(typeof(string))] public string Name { get; set; } [TrimAttribute(typeof(string))] public string Hobby { get; set; } [TrimAttribute(typeof(string))] public string Address { get; set; } }View Code
//擴展方法必須是靜態類,靜態方法。 public static class TrimAttributeExtension { public static void Trim(this object obj) { Type t = obj.GetType(); //得到myclass實例對象的所有屬性 foreach (PropertyInfo prop in t.GetProperties()) { //得到某個屬性上的所有特性 foreach(var attr in prop.GetCustomAttributes(typeof(TrimAttribute),true)) { TrimAttribute trimAttribute = (TrimAttribute)attr; //獲得obj的prop屬性的值 object o=prop.GetValue(obj, null); //如果o不為null且這個屬性上的特性實例的MyType屬性是string類型 if (o!= null && (trimAttribute.MyType == typeof(string))) { //重新給這個屬性賦值,也就是已經Trim()後的,可以看到GetPropertyValue(obj, prop.Name)其實就是o。 object newValue = GetPropertyValue(obj, prop.Name).ToString().Trim(); prop.SetValue(obj, newValue, null); } } } } //拿到屬性本身所表示的值 private static object GetPropertyValue(object instance, string propertyName) { //首先得到instance的Type對象,然後調用InvokeMember方法, //這個方法的第一個參數意思是你需要調用的屬性、方法、字段的”名字“,第二個參數是你調用propertyName是要干什麼, //這裡是拿到屬性,第四個是要操作的實例。最後是需要傳入的參數,這裡調用屬性因此不需要參數我就設置為null了。 return instance.GetType().InvokeMember(propertyName, BindingFlags.GetProperty, null, instance, null); } }View Code
class Program { static void Main(string[] args) { MyClass myclass = new MyClass(); myclass.Name = "小方 "; myclass.Hobby = " 籃球 "; myclass.Address = " 湖北"; myclass.Trim(); /* 執行到這裡會看到上面三個屬性的值中空格全部都沒有了 myclass.Name = "小方"; myclass.Hobby = "籃球"; myclass.Address = "湖北"; */ Console.ReadLine(); } }View Code
3.總體上認識Attribute
特性,這個描述信息的數據類所描述的信息其實就是元數據。當我們在VS中生成解決方案時,在debug文件夾中就會出現一個exe文件,在windows中它稱為可遷移可執行文件PE。PE由3部分組成:PE標頭、IL、元數據。PE頭主要作用是標識此文件是PE文件並說明在內存中執行程序的入口點。IL不用多說,但有一點要注意IL指令中常有元數據標記。元數據包含元數據表和堆數據結構。一個程序中會有很多類,這些類在PE中都會記錄在一個記錄類型的元數據表中,此外還有記錄方法、字段等成員的元數據表,元數據表也可以引用其他的表和堆。可將這些表理解為數據庫中的表,表之間通過主外鍵來建立一種約束與聯系。不過我不知道這些表是如何創建的,是程序中某種成員的所有數據全部放在一起,還是有些數據比如字段是以類為劃分的。元數據的堆數據結構有4種,分別是字符串、Blob、用戶字符串、GUID。在IL中還有一個元數據標記,可以理解為一個指向元數據的指針,它包含4字節。第一個字節說明這個指針指向的類型,比如是指向類表呢還是指向方法表呢。後3個字節說明指向目標表中的位置,這種感覺有點像zigbee編程。再來看元數據的作用,在程序中定義的所有成員以及外部引入的成員都將在元數據中進行說明,這樣在JIT生成機器指令時正是通過元數據中的信息來完成即時編譯的。元數據中存儲程序中程序集的說明(名稱、版本、依賴的其他程序集等),類型的說明(類成員、可訪問性、繼承實現關系等),特性。到這裡可以理解特性是屬於PE中的元數據的一部分,具體到物理結構上我覺得是有一個元數據特性表,比如類型元數據表的一個類有一個指針指向它的元數據特性表,這個特性表記錄著與這個類關聯的所有特性。另外由於特性是作為元數據的一部分,因此特性類將會在編譯時就實例化,而不是運行期動態實例化。