1. 程式人生 > >歡迎來到 C# 9.0(Welcome to C# 9.0)【純手工翻譯】

歡迎來到 C# 9.0(Welcome to C# 9.0)【純手工翻譯】

> 翻譯自 Mads Torgersen 2020年5月20日的博文[《Welcome to C# 9.0》](https://devblogs.microsoft.com/dotnet/welcome-to-c-9-0/),Mads Torgersen 是微軟 C# 語言的首席設計師,也是微軟 .NET 團隊的專案群經理。 C# 9.0 正在成形,我想和大家分享一下我們對下一版本語言中新增的一些主要特性的想法。 對於 C# 的每一個新版本,我們都在努力讓常見的編碼場景的實現變得更加清晰和簡單,C# 9.0 也不例外。這次特別關注的是支援資料模型的簡潔和不可變表示。 就讓我們一探究竟吧! ## 一、僅初始化(init-only)屬性 物件初始化器非常棒。它們為型別的客戶端提供了一種非常靈活和可讀的格式來建立物件,並且特別適合於巢狀物件的建立,讓你可以一次性建立整個物件樹。這裡有一個簡單的例子: ```csharp new Person { FirstName = "Scott", LastName = "Hunter" } ``` 物件初始化器還使型別作者不必編寫大量的建構函式——他們所要做的就是編寫一些屬性! ```csharp public class Person { public string FirstName { get; set; } public string LastName { get; set; } } ``` 目前最大的限制是屬性必須是**可變的**(*譯者注:即可寫的*),物件初始化器才能工作:它們首先呼叫物件的建構函式(本例中是預設的無引數建構函式),然後賦值給屬性 `setter`。 僅初始化(init-only)屬性解決了這個問題!它引入了一個 `init` 訪問器,它是 `set` 訪問器的變體,只能在物件初始化時呼叫: ```csharp public class Person { public string FirstName { get; init; } public string LastName { get; init; } } ``` 有了這個宣告,上面的客戶端程式碼仍然是合法的,但是隨後對 `FirstName` 和 `LastName` 屬性的任何賦值都是錯誤的。 ### 初始化(`init`) 訪問器和只讀(`readonly`)欄位 因為 `init` 訪問器只能在初始化期間呼叫,所以允許它們更改封閉類的只讀(`readonly`)欄位,就像在建構函式中一樣。 ```csharp public class Person { private readonly string firstName; private readonly string lastName; public string FirstName { get => firstName; init => firstName = (value ?? throw new ArgumentNullException(nameof(FirstName))); } public string LastName { get => lastName; init => lastName = (value ?? throw new ArgumentNullException(nameof(LastName))); } } ``` ## 二、記錄(record) > 譯者注: > 原文中宣告一個記錄的 `data class **` 聯合關鍵字現在已經變成 `record` 關鍵字了,所以翻譯過程中做了修正。 如果您想使單個屬性不可變,那麼僅初始化(init-only)屬性是極好的。如果您想要整個物件是不可變的,行為像一個值,那麼你應該考慮宣告它為一個*記錄(record)*: ```csharp public record Person { public string FirstName { get; init; } public string LastName { get; init; } } ``` 對於記錄(`record`),賦予了它一些類似值的行為,我們將在下面深入探討。一般來說,記錄更應該被看作是“值”——資料(`data`),而不是物件!它們並不具有可變的封裝狀態,相反,您需要通過建立表示新狀態的新記錄來表示其隨時間的變化。它們不是由它們的身份(identity)確定的,而是由它們的內容確定的。 ### `with` 表示式 當使用不可變資料(`data`)時,一種常見的模式是從現有的值中建立新值來表示新狀態。例如,如果我們的 `person` 要更改他們的 `LastName`,我們會將其表示為一個新物件,該物件是舊物件的副本,只是有不同的 `LastName`。這種技巧通常被稱之為*非破壞性突變(non-destructive mutation)*。記錄(`record`)不是代表 `person` *在一段時間內的* 狀態,而是代表 `person` *在給定時間點的* 狀態。 為了幫助實現這種程式設計風格,記錄(`record`)允許使用一種新的表示式 —— `with` 表示式: ```csharp var otherPerson = person with { LastName = "Hanselman" }; ``` `with` 表示式使用物件初始化器語法來宣告新物件與舊物件的不同之處。您可以指定多個屬性。 記錄(`record`)隱式定義了一個受保護的(`protected`)“複製建構函式”——一個接受現有記錄物件並逐欄位將其複製到新記錄物件的建構函式: ```csharp protected Person(Person original) { /* copy all the fields */ } // generated ``` `with` 表示式會呼叫“複製建構函式”,然後在上面應用物件初始化器來相應地變更屬性。 如果您不喜歡生成的“複製建構函式”的預設行為,您可以定義自己的“複製建構函式”,它將被 `with` 表示式捕獲。 ### 基於值的相等(Value-based equality) 所有物件都從物件類(`object`)繼承一個虛的 `Equals(object)` 方法。這被用作是當兩個引數都是非空(`non-null`)時,靜態方法 `Object.Equals(object, object)` 的基礎。 結構體重寫了 `Equals(object)` 方法,通過遞迴地在結構體的每一個欄位上呼叫 `Equals` 來比較結構體的每一個欄位,從而實現了“基於值的相等”。**記錄(`record`)是一樣的。** 這意味著,根據它們的“值性(value-ness)”,兩個記錄(`record`)物件可以彼此相等,而不是*同一個*物件。例如,如果我們將被修改 `person` 的 `LastName` 改回去: ```csharp var originalPerson = otherPerson with { LastName = "Hunter" }; ``` 現在我們將得到 `ReferenceEquals(person, originalPerson)` = `false`(它們不是同一個物件),但是 `Equals(person, originalPerson)` = `true`(它們有相同的值)。 如果您不喜歡生成的 `Equals` 重寫的預設逐個欄位比較的行為,您可以自己編寫。您只需要注意理解“基於值的相等”是如何在記錄(`record`)中工作的,特別是在涉及繼承時,我們後面會講到。 除了基於值的 `Equals` 之外,還有一個基於值 `GetHashCode()` 的重寫。 ### 資料成員(Data members) 絕大多數情況下,記錄(`record`)都是不可變的,僅初始化(init-only)公共屬性可以通過 `with` 表示式進行非破壞性修改。為了對這種常見情況進行優化,記錄(`record`)更改了 `string FirstName` 這種形式的簡單成員宣告的預設含義,與其他類和結構體宣告中的隱式私有欄位不同,它被當作是一個公共的、僅初始化(init-only) 自動屬性的簡寫!因此,宣告: ```csharp public record Person { string FirstName; string LastName; } ``` 與我們之前的宣告意思完全一樣,即等同於宣告: ```csharp public record Person { public string FirstName { get; init; } public string LastName { get; init; } } ``` 我們認為這有助於形成漂亮而清晰的記錄(`record`)宣告。如果您確實需要私有欄位,只需顯式新增 `private` 修飾符: ```csharp private string firstName; ``` ### 位置記錄(Positional records) 有時,對記錄(`record`)採用位置更明確的方法是有用的,其中它的內容是通過建構函式引數提供的,並且可以通過位置解構來提取。 完全可以在記錄(`record`)中指定自己的建構函式和解構函式: ```csharp public record Person { string FirstName; string LastName; public Person(string firstName, string lastName) => (FirstName, LastName) = (firstName, lastName); public void Deconstruct(out string firstName, out string lastName) => (firstName, lastName) = (FirstName, LastName); } ``` 但是有一種更簡短的語法來表達完全相同的意思(引數名稱包裝模式`modulo casing of parameter names`): ```csharp public record Person(string FirstName, string LastName); ``` 它聲明瞭公共的僅初始化(init-only)自動屬性以及建構函式和解構函式,因此您就可以編寫: ```csharp var person = new Person("Scott", "Hunter"); // 用位置引數構造(positional construction) var (f, l) = person; // 用位置引數解構(positional deconstruction) ``` 如果不喜歡生成的自動屬性,您可以定義自己的同名屬性,生成的建構函式和解構函式將只使用您自定義的屬性。 ### 記錄與可變性(Records and mutation) 記錄(`record`)的基於值的語義不能很好地適應可變狀態。想象一下,將一個記錄(`record`)物件放入字典中。再次查詢它依賴於 `Equals` 和 `GetHashCode`(有時)。但是如果記錄改變了狀態,它的 `Equals` 值也會隨之改變,我們可能再也找不到它了!在雜湊表實現中,它甚至可能破壞資料結構,因為位置是基於它的雜湊碼得到的。 記錄(`record`)內部的可變狀態或許有一些有效的高階用法,特別是對於快取。但是重寫預設行為以忽略這種狀態所涉及的手工工作很可能是相當大的。 ### `with` 表示式和繼承(With-expressions and inheritance) 眾所周知,基於值的相等和非破壞性突變與繼承結合在一起時是極具挑戰性的。讓我們在執行示例中新增一個派生的記錄(`record`)類 `Student`: ```csharp public record Person { string FirstName; string LastName; } public record Student : Person { int ID; } ``` 然後,讓我們從 `with` 表示式示例開始,實際地建立一個 `Student`,但將它儲存在 `Person` 變數中: ```csharp int newId =