C#筆記 – 定制特性

  • 「自定義特性」只是將一些附加信息與某個目標元素關聯起來的方式

    • 編譯器在托管模塊的元數據中生成這些額外的信息

    • 最常應用特性的記錄項:

      • TypeDef

      • MethodDef

      • ParamDef

      • FieldDef

      • PropertyDef

      • EventDef

      • AssemblyDef

      • ModuleDef

  • 「自定義特性」實際上是一個「類型的實例」,派生自System.Attribute

    • 而「類」必須有公共構造器才能創建它的實例

      • 將特性應用於目標元素時,類似於調用其實例構造器

        • 應用特性時,向其「構造器」傳遞的參數為:「定位參數」

        • 該參數是強制性必須指定

      • 另外也可以設置與特性類關聯的公共字段/屬性

        • 用於設置這些字段/屬性的參數為:「命名參數」

        • 不一定要指定

    • [DllImport("TestDLL", CharSet = CharSet.Auto, SetLastError = true)]
      public static extern void TestMethod();      
      • “TestDLL” 為「定位參數」;CharSet = CharSet.Auto和SetLastError = true為「命名參數」

自定義特性類
  • 特性類必須繼承自Attribute

    • public class MyAttribute : Attribute{}
    • 應把特性類想成一個「邏輯狀態容器」

      • 類的內容很簡單

      • 應只提供一個公共構造器來接受特性的「定位參數」

      • 可以提供公共字段和屬性來接受「命名參數」

      • 不應提供任何公共方法、事件等其他成員

    • 如果要讓一個特性向編譯器指定它的合法應用範圍,需要向特性類應用System.AttributeUsageAttribute特性

      • 由於特性本質上是一個類的實例,因此,其作為一個類本身也可以應用其他特性

      • [AttributeUsage(AttributeTargets.Enum, Inherited = false)]
        public class MyAttribute : Attribute{}
  • 特性構造器可接受的字段/屬性數據類型都是一些簡單的數據類型,如

    • bool/char/byte/sbyte/int/uint/single/double/string/type/object/enum

    • 可使用上述類型的SZ數組,但應盡量避免使用。如果獲取數組作為參數,將會失去與CLS的相容性

    • 應用特性時必須傳遞一個編譯時的常量表達式,它與特性類定義的類型匹配

  • 當編譯器檢測到向目標元素應用了定制特性後,會調用特性的類構造器,向它傳遞任何指定的參數,從而構造特性類的實例

    • 然後,編譯器採用增強型構造器語法所指定的值,對任何公共字段/屬性進行初始化

    • 最後,編譯器將其狀態序列化到目標元素的元數據表記錄項中

    • [MyAttribute]
      public enum TestAttribute
      {
         None
      }

      [AttributeUsage(AttributeTargets.Enum)]
      public class MyAttribute : Attribute
      {
         public MyAttribute(){}
      }
    • TypeDef #1 (02000002)
      -------------------------------------------------------
      TypDefName: CLR_Ch18.TestAttribute (02000002)
      Flags     : [Public] [AutoLayout] [Class] [Sealed] [AnsiClass] (00000101)
      Extends   : 0100000C [TypeRef] System.Enum
      Field #1 (04000001)
      -------------------------------------------------------
      Field Name: value__ (04000001)
      Flags     : [Public] [SpecialName] [RTSpecialName] (00000606)
      CallCnvntn: [FIELD]
      Field type: I4

      Field #2 (04000002)
      -------------------------------------------------------
      Field Name: None (04000002)
      Flags     : [Public] [Static] [Literal] [HasDefault] (00008056)
      DefltValue: (I4) 0
      CallCnvntn: [FIELD]
      Field type: ValueClass CLR_Ch18.TestAttribute

      CustomAttribute #1 (0c00000b)
      -------------------------------------------------------
      CustomAttribute Type: 06000004
      CustomAttributeName: CLR_Ch18.MyAttribute :: instance void .ctor()
      Length: 4
      Value : 01 00 00 00                                     >               <
      ctor args: ()
  • 理解定制特性

    • 它是類的實例,被序列化成駐留在數據中的字節流

    • 運行時可對元數據中的字節進行反序列化,構造出特性類的實例

    • 實際發生:

      • 編譯器在元數據中生成創建特性類的實例所需的信息

      • 每個構造器參數都是1字節的類型ID,後跟具體的值

      • 對構造器進行「序列化」時,編譯器先寫入字段/屬性名,再跟上1字節的類型ID,最後是具體的值

        • 如果是數組,則會先保存數組元素的個數,再跟上每個單獨的元素

自定義特性的檢測
  • 通過「反射」來檢測

    • IsDefined

      • [MyAttribute]
        public enum TestAttribute
        {
           None
        }

        class Program
        {
           static TestAttribute attr = TestAttribute.None;
           public static void TestMethod_A()
          {
               if(attr.GetType().IsDefined(typeof(MyAttribute), false))
              {
                   Console.WriteLine("Is MyAttribute");
              }
               else
              {
                   Console.WriteLine("Not MyAttribute");
              }
          }
        }        

        [AttributeUsage(AttributeTargets.Enum)]
        public class MyAttribute : Attribute
        {
           public MyAttribute(){}
        }
      • IsDefined要求系統查看枚舉類型的元數據,檢查是否關聯了對象特性類的實例

      • 只判斷目標是否應用了一個特性,使用時,不會調用特性對象的構造器或設置其包含的字段/屬性

    • GetCustomAttribute(s)

      • 會構造指定特性類型的新實例,其返回對該特性實例的引用

  • 無論是調用以上哪個方法,內部都須掃描托管模塊的元數據,執行字符串比較來定位指定的定制特性類

匹配特性實例
  • System.Attribute雖然重寫了Object的Equals方法,但是比較低效

    • 首先會在內部比較兩個對象的類型

    • 然後會使用反射比較兩個特性對象中每個字段的值

  • System.Attribute還公開了虛方法Match讓我們重寫

參考書目

  • 《CLR via C#》(第4版) Jeffrey Richter