C#中的Attribute詳解(下)


原文地址:https://blog.csdn.net/xiaouncle/article/details/70229119

C#中的Attribute詳解(下)

一、Attribute本質

從上篇里我們可以看到,Attribute似乎總跟public、static這些關鍵字(Keyword)出現在一起。莫非使用了Attribute就相當於定義了新的修飾符(Modifier)嗎?讓我們一窺究竟吧! 
示例代碼如下:

#define Guo using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Text; namespace AttributeTest { class Program { static void Main(string[] args) { Func(); Console.ReadKey(); } [Conditional("Guo")] static void Func() { Console.WriteLine("Hello Attribute!"); } } }

 

先編譯程序,然后使用微軟的中間語言反編譯器查看MSIL中間語言中static void Func()方法的代碼,截圖如下: 
這里寫圖片描述
可以看出:Attribute本質上就是一個類,它附着在目標對象上最終實例化。

仔細觀察中間語言(MSIL)的代碼之后,那些被C#語言掩蓋的事實,在中間語言中就變得赤身裸體了,Attribute也變得毫無秘密! 
圖中紅色部分指的是Func方法及其修飾符,但Attribute並沒有出現在這里。 
圖中藍色部分指的是調用mscorlib.dll程序集中System.Diagnostics命名空間中ConditionalAttribute類的含參構造方法,01 00 03 47 75 6F 00 00 實際上是字符串Guo的十六進制形式。 
可見,Attribute並不是修飾符,而是一個有着獨特實例化形式的類。

除了分析中間語言之外,給方法添加特性時系統給出的提示信息,也可以幫助大家了解Attribute,系統提示截圖如下: 
這里寫圖片描述

二、Attribute實例化

就像牡蠣天生要吸附在礁石或船底上一樣,Attribute的實例一構造出來就必須“粘”在一個什么目標上。 
Attribute實例化的語法是相當怪異的,主要體現在以下三點:

  1. 不通過new操作符來產生實例,而是使用在方括號里調用構造方法來產生實例。
  2. 方括號必須緊挨着放置在被附着目標的前面。
  3. 因為方括號里空間有限,所以不能使用對象初始化器給對象的屬性(Property)賦值,必須使用含參構造方法對Attribute實例中的屬性賦值。

Attribute實例化時尤其要注意的是:

  1. 構造函數的參數一定要寫。有幾個就得寫幾個,否則類無法正常實例化。
  2. 構造函數參數的順序不能錯。調用任何函數都不能改變參數的順序,除非他有相應的重載(Overload)。因為這個順序是固定的,有些書里稱其為“定位參數”(意即“個數和位置固定的參數”)。
  3. 對Attribute實例中的屬性的賦值可有可無。它會有一個默認值,並且屬性賦值的順序不受限制。有些書里稱屬性賦值的參數為“具名參數”。

三、Attribute實例化的獨特之處

1、他的實例是用.custom聲明的。查看中間語法,你會發現.custom是專門用來聲明自定義特性的。 
2、聲明Attribute的位置是在函數體內的真正代碼(IL_0000至IL_0014)之前。 
3、這就從“底層”證明了Attribute不是“修飾符”,而是一種實例化方式比較特殊的類。

四、元數據的作用

MSIL中間語言中,程序集的元數據(Metadata)記錄了這個程序集里有多少個namespace、多少個類、類里有什么成員、成員的訪問級別是什么。元數據是以文本(也就是Unicode字符)形式存在的,使用.NET的反射(Reflection)技術就能把它們讀取出來,並形成MSIL中的樹狀圖、VS里的Object Browser視圖以及代碼自動提示功能,這些都是元數據與反射技術結合的產物。一個程序集(.exe或.dll)能夠使用包含在自己體內的元數據來完整地說明自己,而不必像C/C++那樣帶着一大捆頭文件,這就叫作“自包含性”或“自描述性”。

五、自定義Attribute實例

在此我們不使用.Net Framework中的各種Attribute系統特性,而是從頭自定義一個全新的Attribute類。 
示例代碼如下:

namespace AttributeTest
{
    class Program
    {
        static void Main(string[] args) { System.Reflection.MemberInfo memberInfo = typeof(Student); System.Reflection.PropertyInfo propertyInfo = typeof (Student).GetProperty("Name"); HobbyAttribute hobbyStudent = (HobbyAttribute)Attribute.GetCustomAttribute(memberInfo, typeof(HobbyAttribute)); HobbyAttribute hobbyName = (HobbyAttribute) Attribute.GetCustomAttribute(propertyInfo, typeof (HobbyAttribute)); if (hobbyStudent != null) { Console.WriteLine("類Student的特性"); Console.WriteLine("類名:{0}", memberInfo.Name); Console.WriteLine("興趣類型:{0}", hobbyStudent.Type); Console.WriteLine("興趣指數:{0}\n", hobbyStudent.Level); } if (hobbyName != null) { Console.WriteLine("屬性Name的特性"); Console.WriteLine("屬性名:{0}", propertyInfo.Name); Console.WriteLine("興趣類型:{0}", hobbyName.Type); Console.WriteLine("興趣指數:{0}", hobbyName.Level); } Console.ReadKey(); } } [Hobby("Sport",Level = 5)] class Student { [Hobby("Tennis",Level = 3)] public string Name { get; set; } public int Age { get; set; } } } namespace AttributeTest { class HobbyAttribute:Attribute { //值為null的string是危險的,所以必需在構造函數中賦值 public HobbyAttribute(string type) { this.Type = type; } public string Type { get; set; } public int Level { get; set; } } }

 

為了不讓代碼太長,上面示例中Hobby類的構造函數只有一個參數,所以對“定位參數”體現的不夠淋漓盡致。大家可以為Hobby類再添加幾個屬性,並在構造函數里多設置幾個參數,體驗一下Attribute實例化時對參數個數及參數位置的敏感性。 
示例運行結果如下: 
這里寫圖片描述

六、Attribute的附着目標

Attribute可以將自己的實例附着在什么目標上呢?這個問題的答案隱藏在AttributeTargets這個枚舉類型里。 
這個枚舉類型的可取值集合為:


All          Assembly   Class     Constructor 
Delegate       Enum    Event     Field 
GenericParameter  Interface   Method    Module 
Parameter      Property  ReturnValue  Struct


一共是16個可取值。不過,上面這張表是按字母順序排列的,並不代表它們真實值的排列順序。使用下面這個小程序可以查看每個枚舉值對應的整數值。

static void Main(string[] args)
{            
    Console.WriteLine("Assembly\t\t{0}", Convert.ToInt32(AttributeTargets.Assembly)); Console.WriteLine("Module\t\t\t{0}", Convert.ToInt32(AttributeTargets.Module)); Console.WriteLine("Class\t\t\t{0}", Convert.ToInt32(AttributeTargets.Class)); Console.WriteLine("Struct\t\t\t{0}", Convert.ToInt32(AttributeTargets.Struct)); Console.WriteLine("Enum\t\t\t{0}", Convert.ToInt32(AttributeTargets.Enum)); Console.WriteLine("Constructor\t\t{0}", Convert.ToInt32(AttributeTargets.Constructor)); Console.WriteLine("Method\t\t\t{0}", Convert.ToInt32(AttributeTargets.Method)); Console.WriteLine("Property\t\t{0}", Convert.ToInt32(AttributeTargets.Property)); Console.WriteLine("Field\t\t\t{0}", Convert.ToInt32(AttributeTargets.Field)); Console.WriteLine("Event\t\t\t{0}", Convert.ToInt32(AttributeTargets.Event)); Console.WriteLine("Interface\t\t{0}", Convert.ToInt32(AttributeTargets.Interface)); Console.WriteLine("Parameter\t\t{0}", Convert.ToInt32(AttributeTargets.Parameter)); Console.WriteLine("Delegate\t\t{0}", Convert.ToInt32(AttributeTargets.Delegate)); Console.WriteLine("ReturnValue\t\t{0}", Convert.ToInt32(AttributeTargets.ReturnValue)); Console.WriteLine("GenericParameter\t{0}", Convert.ToInt32(AttributeTargets.GenericParameter)); Console.WriteLine("All\t\t\t{0}", Convert.ToInt32(AttributeTargets.All)); Console.ReadKey(); }

 

運行結果如下: 
這里寫圖片描述 
它們的值並不是步長值為1的線性遞增,除了All之外,每個值的二進制形式中只有一位是“1”,其余全是“0”。這就是枚舉值的另一種用法——標識位。那么標識位有什么好處呢? 
如果我們的Attribute要求既能附着在類上,又能附着在方法上,可以使用C#中的操作符”|”(即按位求“或”)。有了它,我們只需要將代碼書寫如下:AttributeTargets.Class | AttributeTargets.Method,因為這兩個枚舉值的標識位(也就是那個唯一的1)是錯開的,所以只需按位求或就解決問題了。這樣,你就能理解為什么AttributeTargets.All的值是32767了。 
默認情況下,當我們聲明並定義一個新的Attribute類時,它的可附着目標是AttributeTargets.All。大多數情況下,AttributeTargets.All就已經滿足要求了。不過,如果你非要對它有所限制,那就要費點兒周折了。 
例如,你想把前面的HobbyAttribute類的附着目標限制為只有“類”和“屬性”能使用,則示例代碼如下:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property)] class HobbyAttribute : Attribute { //HobbyAttribute類的具體實現 }

 

這里使用Attribute的實例(AttributeUsage)附着在Attribute類(HobbyAttribute)上。Attribute的本質是類,而AttributeUsage又說明HobbyAttribute可以附着在哪些類型上。

七、附加問題

1、如果一個Attribute類附着在了某個類上,那么這個Attribute類會不會隨着繼承關系也附着到派生類上呢? 
2、可不可以像多個牡蠣附着在同一艘船上那樣,讓一個Attribute類的多個實例附着在同一個目標上呢? 
答案:可以。代碼如下:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property,Inherited = false,AllowMultiple = true)] class HobbyAttribute : Attribute { //HobbyAttribute類的具體實現 }

 

AttributeUsage這個專門用來修飾Attribute的Attribute,除了可以控制修飾目標外,還能決定被它修飾的Attribute是否可以隨宿主“遺傳”,以及是否可以使用多個實例來修飾同一個目標! 
那修飾ConditionalAttribute的AttributeUsage又會是什么樣子呢?(答案在MSDN中)

參考文章: 
深入淺出Attribute (上)——Attribute初體驗 
深入淺出Attribute (中)——Attribute本質論 
我很想知道寫這兩篇文章的作者,他是在哪里獲得這些知識的,或者說他在寫這兩篇文章時又參考了哪些資料呢?

版權聲明:本文為博主原創文章,未經博主允許不得轉載。 https://blog.csdn.net/xiaouncle/article/details/70229119


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM