在 stackoverflow 上面看到的一個問題,覺得有點意思。接著,爬了一些文,發現這個問題至少在三、四年前就有人反映,直到最近幾天都還有人在討論。所以我稍微整理一下這個問題的來龍去脈,就當作是〈C# 的唯讀自動屬性是怎樣煉成的〉續集吧。

問題描述

請先看底下這段程式碼:

interface IEmployee
{
    int Salary { get; }   // 唯讀屬性。
}

class Employee : IEmployee
{
    public int Salary { get; } // 隱含實作 IEmployee.Salary 屬性。

    public Employee()
    {
        Salary = 70000;     // 在建構函式中初始化唯讀屬性 Salary。
    }
}

說明:
  • 介面 IEmployee 只定義了一個成員:Salary,它是個唯讀屬性。 
  • 類別 Employee 隱含實作了 IEmployee.Salary 屬性。由於它是唯讀自動屬性,故在建構函式中設定其初始值。 

剛才的程式碼沒有問題,可以通過編譯。這裡有 .NET Fiddle 的連結可以直接測試:
https://dotnetfiddle.net/y5l8aJ(必須把 Compiler 指定為 Roslyn 2.0)。

那麼,如果我們想要改成明確實作(explicitly implement)介面的寫法,直覺上可能會這樣寫:

class Employee : IEmployee
{
    int IEmployee.Salary { get; } // 明確實作 IEmployee.Salary 屬性。

    public Employee()
    {
        Salary = 70000;     // 編譯失敗: 此處無法使用 Salary!
    }
}

倒數第 3 行無法通過編譯,錯誤訊息是:
The name 'Salary' does not exist in the current context. (在目前的區塊裡面不存在 'Salary' 這個東西。)
這是因為明確實作介面的時候,不能在類別裡面直接以名稱來存取介面的成員。

那麼,把發生錯誤的那行程式碼改成底下這樣試試:

    (this as IEmployee).Salary = 70000;


這也行不通,編譯器會告訴你:
Property or indexer 'IEmployee.Salary' cannot be assigned to -- it is read only.
因為 Salary 本來就是定義成唯讀屬性,不可修改。

可是,回頭再看一下第一個範例,當我們採用隱含實作介面的寫法時,Salary 也是定義成唯讀屬性啊;而且,自動唯讀屬性本來就可以在建構函式裡面設定初始值(C# 6)。顯然,這條語法規則一旦碰到明確實作介面的場合,就行不通了。

解決方法

目前看來,若一定要採用明確實作介面的寫法,就只能繞個彎,用唯讀的私有欄位來解決了。像這樣:

class Employee : IEmployee
{
    private readonly int _salary;   // 唯讀的私有欄位。

    int IEmployee.Salary    // 明確實作 IEmployee.Salary 屬性。
    {
        get { return _salary; }
    }

    public Employee()
    {
        _salary = 70000;
    }
}

如果要再簡潔一點,可以用「以表示式為本體的成員」(expression-bodied members)語法:

class Employee : IEmployee
{
    private readonly int _salary;   // 唯讀的私有欄位。

    int IEmployee.Salary => _salary; // 明確實作 IEmployee.Salary 屬性。

    public Employee()
    {
        _salary = 70000;
    }
}

小結

這篇筆記的重點是:
  • 唯讀自動屬性可以在建構函式中設定初始值。這沒有違反 C# 語法。
  • 如果採用明確實作介面的方式,上述作法行不通。

目前這個問題在 C# 語言設計的官方 github repo 上面仍有人在持續討論。有興趣的話,可以 follow 看看後續如何發展。

參考資料