這篇文章要說明的是 C# 10 對記錄類型(record type)做了哪些改進。
記錄(record)是從 C# 9 開始提供,而 C# 10 有兩處強化:
- 宣告記錄類型時,可明確指定以結構(struct)作為真實型別。
- 在記錄中改寫
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!
沒有留言: