C# 的唯讀自動屬性是怎樣煉成的

這兩天,我把《C# 本事》第六章有關於 C# 6 唯讀自動屬性的部分再細化、完善。這個部分尚未發布至電子書平台,我先把它摘錄在這裡,稍加排版,供有需要的朋友參考。

C# 1

在 C# 1,每⼀個屬性通常會有對應的私有欄位,以及⼀對⽤來讀寫屬性的⽅法,分別稱為 getter 和 setter。如以下範例所⽰:

// C# 1
public class Employee
{
    private string _id;  // 屬性背後的實際欄位(backing field)

    public string ID
    {
        get { return _id; }  // 屬性的讀取方法(getter)
        set { _id = value; } // 屬性的設定方法(setter)
    }
}
註:屬性的「讀取方法」和「設定方法」有點拗口,不如用英文來得簡單而且不易混淆,所以接下來會直接寫 getter 和 setter。

有了 getter 和 setter,類別的私有欄位就等於是多了一層保護,不但可防止外界任意修改私有欄位(例如在 setter 裡面檢核外界傳入的 value 是否合法),還可以在 getter 裡面加入一些額外的運算邏輯來決定該回傳給外界什麼內容。

C# 2

到了 C# 2,getter 和 setter 可以限定存取範圍,例如 protected 和 private。其中一種很常見的用法,就是公開的 getter 搭配 私有的 setter,讓外界只能讀取屬性值,而不能修改它——也就是唯讀屬性的意思。像這樣:

// C# 2
public class Employee
{
    private string _id;

    public string ID
    {
        get { return _id; } // 公開的 getter 不必、也不可加上 `public`。
        private set { _id = value; } // 只有類別自己才能修改屬性值。
    }
}

值得一提的是,若 getter 或 setter 要讓外界存取,則不必、也不可以在前面加上 public 修飾詞。這是因為屬性的 getter 和 setter 的存取範圍必須比屬性宣告時的存取範圍更「窄」。以剛才的範例來說,屬性 ID 已經宣告為 public,那麼如果要為它的 getter 或 setter 指定存取範圍,就只能用 internal、protected、或 private。

C# 3

C# 3 增加了自動實作屬性(automatically implemented properties),或簡稱「自動屬性」。這個新語法能夠讓我們在定義屬性時省去宣告私有欄位的麻煩,當類別裡面的屬性數量很多的時候,這個語法可以讓程式碼簡潔許多。請看底下的範例:

// C# 3:自動實作屬性(免寫私有欄位)。
public class Employee
{
    public string ID { get; private set; }
    public string Name { get; set; }
    public DateTime Birthday { get; set; }
    // 想像底下還有一堆屬性,而我們能夠省下多少私有欄位的宣告。

    public Employee(string id) // 建構子
    {
        ID = id;
    }
}

請注意屬性 ID 可讓外界讀取,但是只有類別本身才能修改其值。所謂的類別本身,當然包含類別的建構子和方法。可是,如果我們希望這個唯讀屬性只允許在建構子中設定一次初始值,以後就再也不能修改了——即使在類別的其他方法中也不能修改,這種情況要怎麼寫呢? C# 5 沒辦法做到——除非使用唯讀的私有欄位,但程式碼寫起來比較囉嗦。

C# 6

到了 C# 6,終於在「不可改變性」(immutability)這方面做了改進,提供更好的寫法,也就是「唯讀自動屬性」(read-only automatically implemented properties)語法,像這樣:

// C# 6:唯讀的自動屬性。
public class Employee
{
    public string ID { get; } // 沒有 setter,這是個唯讀自動屬性。

    public Employee(string id)
    {
        ID = id; // 設定唯讀自動屬性的初始值。
    }

    private void ChangeID(int id)
    {
        ID = id;  // 編譯失敗:ID 是唯讀屬性。
    }
}

如您所見,此處有兩個重點:
  • 屬性 ID 只有 getter,沒有 setter 了。它是個唯讀自動屬性。 
  • 由於 ID 是唯讀屬性,因此即使是類別自己也無法修改 ID 的值——建構子除外。

除了建構子之外,C# 6 還提供了另一個地方可以讓我們設定自動屬性的初始值:在宣告屬性的時候就設定初始值。請接著看下一節。

自動屬性的初始設定式

在 C# 6 之前,自動屬性的初始值只能透過建構子來設定,而無法在宣告屬性的時候就設定初始值。C# 6 對此做了改進,提供了「自動屬性初始設定式」(auto-property initializers)語法,讓程式碼可以再簡潔一點。如以下範例所示:

public class Employee
{
    public string ID { get; private set; } = "A001";
    public string Name { get; set; } = "Michael";
    public int Age { get; } = 20;
}

此語法的主要特色,是在定義自動屬性的時候用 = 運算子來加上賦值敘述,以設定該屬性的初始值。需注意的是,此賦值語法只能用於自動實作屬性;若用在一般的屬性,編譯時會報錯。所以底下的程式碼無法通過編譯:

public class Employee
{
    private string _name;

    public string Name
    {
        get { return _name; }
        set { _name = value; }
    } = "另壺沖"  // 編譯錯誤! 只有自動屬性才允許初始設定式。
}

另外,自動屬性的初始設定式無法透過 this 來存取該類別的其他成員。也就是說,如果想要在自動屬性的初始設定式中呼叫該類別的方法,則該方法必須是靜態方法。請看以範例與註解中的說明:

public class Employee
{
    public int Age { get; } = this.GetAge(); // 編譯失敗!不可呼叫 instance 方法。
    public int Salary { get; } = GetSalary();  // 可以呼叫靜態方法。

    int GetAge() { return 20; }
    static int GetSalary() { return 20000; } // 注意:這是個靜態方法。
}

關於不可變性

在結束之前,我想再提一下唯讀自動屬性所提供的「不可變性」(immutability)在寫程式的時候有什麼好處。請看底下的範例:

public class Employee
{
    public List<string> Addresses { get; } // 唯讀自動屬性
    }

    public Employee() // 在建構子中初始化唯讀自動屬性
    {
        Addresses = new List<string>();
    }

    public void DoSomethingStupid()
    {
        Addresses = null; // 編譯失敗:不允許修改。
    }
}

屬性 Addresses 是個唯讀自動屬性,一旦初始化完成,就不能在其他地方修改。於是乎,外界可以取得 Addresses 串列,並且呼叫它的 Add 或 Remove 方法來增加或刪除元素,但是無法將這個 Addresses 屬性設定成 null 或指向其他串列。參考以下範例:

public static void Main()
{
    var emp = new Employee();
    emp.Addresses.Add("嘉義市大馬路 123 號"); // OK!

    emp.Addresses = new List<string>(); // 編譯失敗:不允許修改。
}
此範例程式的 .NET Fiddle 連結:https://dotnetfiddle.net/9TINL9

以上便是 C# 屬性語法的大致演進歷程。若有遺漏或未盡詳細之處,歡迎補充、指正。

沒有留言:

技術提供:Blogger.
回頂端⬆️