C# 9.0 新特性預覽 – init-only 屬性

C# 9.0 新特性預覽 – init-only 屬性

前言

隨著 .NET 5 發布日期的日益臨近,其對應的 C# 新版本已確定為 C# 9.0,其中新增加的特性(或語法糖)也已基本鎖定,本系列文章將向大家展示它們。

目錄

[C# 9.0 新特性預覽 – 類型推導的 new]
[C# 9.0 新特性預覽 – 空參數校驗]
[C# 9.0 新特性預覽 – 頂級語句]
[C# 9.0 新特性預覽 – init-only 屬性]
[C# 9.0 新特性預覽 – Record 類型]
[C# 9.0 新特性預覽 – 模式匹配的改善]
[C# 9.0 新特性預覽 – 源程式碼生成器]
[C# 9.0 新特性預覽 – 其他小的變化]

只初始化 setter (Init Only Setters)

這個特性允許創建只初始化(init only)的屬性和索引器,使得 C# 中的不可變模型更加靈活。

背景

在此之前,我們創建實體類/POCO類/DTO類等等模型類的時候,都期望屬性只讀不允許從外部修改,會將屬性的 setter 設為私有或者乾脆不設置 setter,例如:

public class Person
{
    public string Name { get; private set; }
    // OR
    //public string Name { get; }
}

再添加一個擁有全部屬性作為簽名的構造方法:

...
public Person(string name)
{
    this.Name = name;
}
...

這樣做雖然可以達到目的,但是帶來了兩個問題
1.假如屬性增多,會帶來工作量的成倍增加,以及不易維護
2.無法使用對象初始化器(object initializers)

var person = new Person
{
    Name = "Rwing" // Compile Error 
};

在這個情況下,init 關鍵字應運而生了。

語法

語法很簡單,只需要將屬性中的 set 關鍵字替換為 init 即可:

public string Name { get; init; }

以上程式碼會被大致翻譯為:

private readonly string _name;
public string Name
{
    get { return _name; }
    set { _name = value;}
}

可以看到,與 set 的區別是,init 會為背後的欄位添加 readonly 關鍵字。
這樣我們就可以去掉一堆屬性的構造方法轉而使用對象初始化器了,並且達到了不可變的目的。

var person = new Person
{
    Name = "Rwing"
};
// 初始化後無法再次修改
person.Name = "Foo"; // Error: Name is not settable

這一語法,有很多場景需要配合約樣在 C# 9.0 中新增的 record 類型使用。

哪些情況下可以被設置

  • 通過對象初始化器
  • 通過 with 表達式
  • 在自身或者派生類的構造方法中
  • 在標記為 init 的屬性中
  • 在特性(attribute)類的命名參數屬性中

以上場景不難理解,但是值得一提的是,只有 get 的屬性是不可以派生類的構造方法中賦值的,但是 init 可以:

class Base
{
    public bool Foo { get; init; }
    public bool Bar { get; }
}

class Derived : Base
{
    Derived()
    {
        Foo = true;
        Bar = true; // ERROR
    }
}

此外有一種例外, 在以上場景中的 lambda 或本地函數中,也不允許被設置,例如:
原因也很簡單,lambda 或本地函數在編譯後已經不在構造函數中了。

public class Class
{
    public string Property { get; init; }
    
    Class()
    {
        System.Action a = () =>
        {
            Property = null; // ERROR
        };
        local();
        void local()
        {
            Property = null; // ERROR
        }
    }
}

參考

[Proposal: Init Only Setters]
[InitOnlyMemberTests.cs]