Attribute parameter types

  • 1646
  • 0
  • 2019-01-26

Attribute 是個古怪又有趣的課題,這篇文章談談 Attribute parameter 的相關議題。

在 C# language specification 的 17.1.3 提到 Attribute parameter types 有以下的限制:

  1. One of the following types: bool, byte, char, double, float, int, long, sbyte, short, string, uint, ulong, ushort.
  2. The type object.
  3. The type System.Type.
  4. An enum type, provided it has public accessibility and the types in which it is nested (if any) also have public accessibility  (§17.2)
  5. Single-dimensional arrays of the above types.

其中 1,2,3 大概沒甚麼問題,很明顯就是屬於這三項規範的型別是可以使用的,5 應該也還好,就是指以上四項規範的一維陣列可以使用;比較有趣的在第四點,翻成中文的意義就是『列舉型別,這個型別的存取範圍必須是公用的(也就是以 public 存取修飾詞宣告),若這個列舉型別是位在某個型別內部的巢狀型別,則該型別也必須是公用存取範圍。』

使用以下的程式碼,我刻意將 enum 與 attribute 都宣告為 internal,這程式碼會通過考驗嗎?
 

namespace ConsoleApp8
{
    class Program
    {
        static void Main(string[] args)
        {
            var attribute = typeof(MyClass).GetCustomAttribute<SomeAttribute>();
            Console.WriteLine(attribute.Current);
            Console.ReadLine();
        }
    }

    internal enum Status
    {
        On, Off
    }

    internal class SomeAttribute : Attribute
    {
        internal Status Current { get; set; }

        internal SomeAttribute(Status current)
        {
            Current = current;
        }
    }

    [Some(Status.Off)]
    public class MyClass
    {

    }
}

各位可以嘗試看看,這個程式碼不僅可以通過編譯,還能夠正確執行。難道 C# 語言規範寫錯了嗎?

不!C# 語言規範並沒有寫錯。

那問題出在哪裡?明明第四點寫的就是 public accessibility ,而程式碼明明就是 internal enum,這個巧妙就在於這個章節的名稱 『Attribute parameter types』。咱們先來宣告一個 custom attribute,如下:(為了避免誤會,所以直接顯式宣告其無參數建構式)。
 

    internal class OtherAttribute : Attribute
    {
        public OtherAttribute()
        { }
        public string Name { get; set; }
        public int X;
    }
​

建構式沒有任何參數宣告,那該如何在其他元素套用 OtherAttribute 時賦予 Name property 和 X field的值呢?我們可以用以下幾個方式套用這個 Attribute:
 

    [Other]
    public class MyClass1
    { }

    [Other()]
    public class MyClass2
    { }

    [Other(Name = "國寶")]
    public class MyClass3
    { }

    [Other(X =1)]
    public class MyClass4
    { }

    [Other(Name="國寶",X = 1)]
    public class MyClass5
    { }

類似 [Other(Name="國寶",X = 1)] 這樣的寫法在一般的類別建構式上是辦不到的,我指的是單純只有在類別內部設定該屬性,而建構式維持無參數的狀態,如果在建構式使用具名參數就是另外一回事了。事實上,在[Other(Name = "國寶", X = 1)] 中小括弧內部的那些也不全然只能放建構式的參數,這也就是為什麼會稱為 attribute parameter types 而不是叫做 attribute constructor parameter types。

Attribute parameters 其實分成兩種:positional parameter 和 named parameter (參考 C# language specification 17.1.2),簡單這麼解釋這兩樣的差別:

Positional parameter

Positional parameter 顧名思義就是靠著位置辨識的參數,說白話一點就是宣告在建構式上的參數,因為建構式上的參數式靠位置決定的。例如以下 attribute 建構式上的 current 就是 positional parameter:
 

    internal class SomeAttribute : Attribute
    {
        internal Status Current { get; set; }

        internal SomeAttribute(Status current)
        {
            Current = current;
        }
    }
Named parameter

Named parameter 就是靠名稱來辨識的參數,例如 OtherAttribute 中的 Name 與 X:
 

    internal class OtherAttribute : Attribute
    {
        public OtherAttribute()
        { }
        public string Name { get; set; }
        public int X;
    }

這裡產生了一個問題,要能夠成為 named parameter 的成員 -- 執行個體屬性 (property) 或欄位 (field) ,它的存取範圍宣告必須是 public。如果我們將先前的範例改成以下,在編譯時期就會產生編譯錯誤 -- CS0617    'Name' 不是有效的具名屬性引數。

    internal class OtherAttribute : Attribute
    {
        public OtherAttribute()
        { }
        internal string Name { get; set; }
        public int X;
    }

    [Other]
    public class MyClass1
    { }

    [Other()]
    public class MyClass2
    { }

    [Other(Name = "國寶")]
    public class MyClass3
    { }

    [Other(X =1)]
    public class MyClass4
    { }

    [Other(Name = "國寶", X = 1)]
    public class MyClass5
    { }

當一個型別不是 public 的狀態,它是不能用於宣告為某個 public property 或 field的型別,也因此,若這個 enum 本身不是 public ,或是包裝著它的巢狀型別不是 public 時,這個 enum 無法用來宣告 public property  或 field,於是乎它就無法成為該 attribute 的 named parameter。這也就是 『An enum type, provided it has public accessibility and the types in which it is nested (if any) also have public accessibility 』這句話所要闡述的意義。