C# 10:Record 的改進

這篇文章要說明的是 C# 10 對記錄類型(record type)做了哪些改進。


記錄(record)是從 C# 9 開始提供,而 C# 10 有兩處強化:

  1. 宣告記錄類型時,可明確指定以結構(struct)作為真實型別。
  2. 在記錄中改寫 ToString 方法時,可將其密封(sealed),以避免其他人——尤其是編譯器——改寫此方法。

接著對此兩處強化功能進一步說明。

以下內容需要具備 record 的基本知識,才比較好理解。可參閱〈C# 9:Record 詳解〉一文的說明。

以結構實作的記錄

C# 9 的記錄(record),其編譯後的程式碼是以類別的形式存在,所以是參考型別。

C# 10 新增了 record struct 語法,讓我們能夠指定使用結構來作為實際型別。例如:

public record struct Point(int X, int Y);

這裡使用了比較簡潔的「位置參數」語法來定義 Point 記錄。以此方式定義的記錄類型,實際上會被編譯成類似底下的程式碼:

public struct Point : IEquatable<Point>
{
	public int X { get; set; }
	public int Y { get; set; }
    ... 其餘省略
}

觀察重點 1:此記錄類型是以結構(struct)來實現(第 1 行),所以它是個實質型別(value type),而非參考型別(reference type)。換言之,它也會有實質型別的限制,例如不支援繼承。

觀察重點 2:以位置參數語法來定義的記錄,編譯器會自動產生對應的屬性;而編譯器為 record struct 產生的屬性並非唯讀,而是可以隨時修改的,如第 3~4 行的屬性 X 與 Y。這是 record struct 和單純宣告 record 的一個主要差異。

如果基於某些原因而必須使用 record struct,同時又希望整個結構是唯讀的,此時有兩種作法,一個是在宣告時加上 readonly 關鍵字:

public readonly record struct Point(int X, int Y);

另一種作法是改用一對大括號的寫法,以便我們可以更細緻地去設計每個屬性的行為:

public record struct Point
{
    public int X { get; init; }
    public int Y { get; init; }
}

如此一來,X 和 Y 便是 init-only 屬性,亦即只能在物件初始化的過程中賦值,隨後無法再修改其值。

ToString 方法可被密封

基礎知識:編譯器會幫我們自訂的 record 類型安插許多程式碼,其中包括改寫的 ToString 方法

然而,當我們有多個自訂的記錄類型,彼此之間有好幾層繼承關係時,編譯器提供的這項功能反倒會出問題:位於繼承階層頂端的記錄如果想要把 ToString 的輸出結果固定下來,不讓後代亂改,這在 C# 9 是辦不到的,因為編譯器總是會替子代記錄改寫 ToString 方法。

於是有人想到,何不在基礎型別裡面加上 sealed 修飾詞來禁止後代修改呢?像這樣:

abstract record Point(int X, int Y)
{
    public sealed override string ToString() // C# 9: Error!
    {
        return $"({X},{Y})";
    }
}

在 C# 9,第 3 行無法通過編譯。錯誤訊息是:

Error CS8773: Feature 'sealed ToString in record' is not available in C# 9.0.

到了 C# 10,上列程式碼便可以通過編譯。如此一來,便可防止他人(特別是編譯器)改寫父代記錄的方法。

實作此功能的 Thomas Levesque 曾在某個留言板裡面說:「this feature isn't really to prevent a user from overriding the method, but to prevent the compiler from doing so.」

Happy coding!

沒有留言:

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