C# 9.0正在形成,我想分享我們對新增到該語言下個版本的一些主要功能的看法。對於每個新版本的 C#,我們努力使常見的編碼方案更加清晰和簡單,C# 9.0 也不例外。這次的一個特別重點是支援資料形狀的簡潔和不可變表示。
讓我們潛入吧!
1 僅可初始化的屬性
物件初始化器是非常好用的。它們為型別例項化提供了一種非常靈活且可讀的格式來建立物件,尤其是對於一次建立特別大的巢狀物件來說。下面是一個簡單的例子:
new Person { FirstName = "Scott", LastName = "Hunter" }
物件初始化也使使用者不必編寫大量建構函式,要做的就是編寫一些屬性!
public class Person { public string FirstName { get; set; } public string LastName { get; set; } }
今天,一個很大的限制是,屬性必須是可修改的,物件初始化器是這樣工作的:首先呼叫物件的建構函式(預設為無參的建構函式),然後分配給屬性設定器(property setter)。
僅可初始化屬性修改了這一點!它們引入了一個 init 訪問器,該訪問器是set訪問器的變體,只能在物件初始化期間呼叫:
public class Person { public string FirstName { get; init; } public string LastName { get; init; } }
使用此宣告,除了初始化外,之後任何後續賦值給 FirstName 和 LastName 屬性都是一個錯誤。
因為init訪問器只能在初始化期間訪問,因此他們允許修改封閉型別中的只讀欄位,就像在建構函式中那樣:
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))); } }
2 記錄
如果要使單個屬性不可變,則僅可初始化屬性非常適合。如果希望整個物件不可變且像值型別一樣,則應考慮將其宣告為記錄:
public data class Person { public string FirstName { get; init; } public string LastName { get; init; } }
類宣告中的data關鍵字將其標記為記錄。這賦予它幾個類似價值型別的行為,我們將在下面深入探討這些行為。一般來說,記錄更被視為"值"(純資料), 而不是作為物件。您可以通過建立新記錄表示新狀態來表示隨時間的變化。它們不是由標識定義,而是由其內容定義。
2.1 With表示式
使用不可變資料時,一種常見模式是從現有值建立新值以表示新狀態。例如,如果我們更改LastName,我們會將其表示為一個新物件,該物件是舊物件的副本,但LastName不同。這種技術通常被稱為非破壞性修改。記錄這種特性表示的是Person在給定時間的狀態。
為了適應這種程式設計風格,記錄允許一種新的表示式——with:
var otherPerson = person with{LastName="Hanselman"};
with表示式使用物件初始化器語法來說明新物件與舊物件的不同內容。您可以指定多個屬性。
記錄隱式定義一個受保護的"複製建構函式"-一個建構函式,它獲取現有記錄物件,並逐個將其欄位複製到新的物件:
protected Person(Person original){/* copy all the fields */}// generated
with 表示式會導致呼叫複製建構函式,然後在上面應用物件初始化器以相應地更改屬性。
如果您不喜歡生成的複製建構函式的預設行為,則可以改為定義自己的行為,該行為將由with表示式選取。
2.2 基於值的相等性
所有物件都從Object繼承 Equals(object)。結構將其重寫為具有"基於價值的相等性",通過遞迴地呼叫Equals來比較結構的每個欄位。記錄也執行相同的操作。這意味著,根據其"值",兩個記錄物件可以彼此相等,而不必是同一物件。例如:
var originalPerson = otherPerson with { LastName = "Hunter" };
現在 ReferenceEquals(person, originalPerson) = false(這兩個不是一個物件)但是Equals(person, originalPerson) = true (他們有相同的值)。
如果您不喜歡生成的 Equals 重寫的預設逐欄位比較行為,則可以改為編寫自己的欄位比較行為。你只需要小心,你瞭解基於值的相等在記錄中是如何工作的,特別是當涉及繼承時。
除了重寫Equals 外,還有 GetHashCode()。
2.3 資料成員
記錄絕大多數都是不可變的,只有只讀初始化器可以通過with表示式進行非破壞性修改。為了針對這種常見情況進行優化,記錄在宣告時會更改string FirstName這類成員宣告的行為。與其他類和結構宣告中的隱式private欄位不同,在記錄中,這被視為public的、僅可初始化的自動屬性的縮寫!因此:
public data classPerson { string FirstName; string LastName; }
與
public data classPerson { public string FirstName{get; init;} public string LastName{get; init;} }
是相同的。
我們認為這有助於做出漂亮而清晰的記錄宣告。如果您真的需要私有欄位,只需顯式地新增private修飾符:
private string firstName;
2.4 基於位置的記錄
有時,對記錄採用更為位置化的方法是有用的,在這種方法中,記錄的內容通過建構函式引數的位置給出,並且可以通過解構函式來提取。
可以在記錄中指定自己的建構函式和解構函式:
public data classPerson { 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); }
上面程式碼可以簡寫為:
public data class Person(string FirstName,string LastName);
這將宣告public的僅初始化的自動屬性以及建構函式和解構函式,以便您可以編寫:
var person =new Person("Scott","Hunter");// positional construction var(f, l)= person; // positional deconstruction
如果您不喜歡生成的自動屬性,則可以改為定義自己的同名屬性,生成的建構函式和解構函式將使用該屬性。
2.5 記錄的改變引發的問題
想象一下,將記錄物件放入字典中。再次找到它取決於 Equal 和GetHashCode。如果記錄改變其狀態,它也會改變它等於什麼!我們可能再也找不到了!在雜湊表實現中,它甚至可能損壞資料結構,因為定位基於的是"到達雜湊表時"的雜湊值!
雖然可以通過重寫一些內部方法來改變這種預設的行為,但其工作量也是相當巨大的。
2.6 with表示式與繼承
public data class Person{string FirstName;string LastName;} public data class Student:Person{int ID;} Person person =new Student{FirstName="Scott",LastName="Hunter", ID =GetNewId()}; otherPerson = person with{LastName="Hanselman"};
在最後一行上使用with表示式時,編譯器不知道person實際上包含了一個Student。而且,即使otherPerson實際上不是"Student"物件,它也不是一個正確的副本,該物件與複製的第一個物件具有相同的ID。
記錄有一個隱藏的虛方法,它委託"克隆"整個物件。每個派生記錄型別都重寫此方法以呼叫該型別的複製建構函式,以及派生鏈上的複製建構函式直到基類記錄的複製建構函式。with表示式只需呼叫隱藏的"克隆"方法,並將物件初始化器應用於結果。
2.7 值相等與繼承
與with表示式的實現類似,基於值的相等性也必須是"虛擬"的,即Student需要比較所有欄位,即使比較時能夠得知型別是基型別Person。這是很容易通過重寫已經虛擬的Equals方法實現的。
但是,相等還有一個挑戰:如果比較兩種不同的Person,該怎麼辦?我們不能讓其中一個決定是否相等:相等應該是對稱的,所以無論兩個物件中哪個是第一個,結果都應該是相同的。換句話說,他們必須就適用的相等達成一致!
說明問題的示例:
Person person1 =new Person{FirstName="Scott",LastName="Hunter"}; Person person2 =new Student{FirstName="Scott",LastName="Hunter", ID =GetNewId()};
這兩個物件彼此相等嗎?person1可能會這樣認為,因為person2有所有的Person的構造,但person2會認為與person1不同!我們需要確保他們都同意他們是不同的物件。
C# 會自動為您處理。它的實現方式是每個記錄都有一個"EqualityContract"的虛擬受保護屬性。每個派生記錄都會重寫它,為了比較相等,兩個物件必須具有相同的EqualityContract。
3 簡化頂級程式
之前我們這樣寫程式碼:
using System; class Program { static void Main() { Console.WriteLine("Hello World!"); } }
現在您可以選擇在頂層編寫主程式:
using System; Console.WriteLine("Hello World!");
支援任何語句,但必須在using之後以及檔案中的任何型別或名稱空間宣告之前,並且只能在一個檔案中執行此操作,就像目前只能有一個Main方法一樣。如果要返回狀態程式碼,可以執行此操作。如果你想await,你可以這樣做。如果要訪問命令列引數,可以訪問args引數。
區域性函式是語句的一種形式,在頂級程式中也允許使用。從頂級語句部分以外的任何位置呼叫它們都是錯誤的。
4 改進模式匹配
在 C# 9.0 中新增了幾種新型別的模式。例如:
public static decimal CalculateToll(object vehicle) => vehicle switch { ... DeliveryTruck t when t.GrossWeightClass > 5000 => 10.00m + 5.00m, DeliveryTruck t when t.GrossWeightClass < 3000 => 10.00m - 2.00m, DeliveryTruck _ => 10.00m, _ => throw new ArgumentException("Not a known vehicle type", nameof(vehicle)) };
4.1 簡單型別模式
目前,型別模式需要在型別匹配時宣告一個識別符號,即使該識別符號是一個_,比如 DeliveryTruck _。新語法不用了,可以簡寫為:
DeliveryTruck => 10.00m,
4.2 關係模式
C#9.0引入了對應於關係運算子<、<=等的模式。因此,新語法可以這樣寫:
DeliveryTruck t when t.GrossWeightClass switch { > 5000 => 10.00m + 5.00m, < 3000 => 10.00m - 2.00m, ... },
4.3 邏輯模式
最後,可以將模式與邏輯運算(and 、or、not)符組合起來,並將其拼寫為單詞,以避免與表示式中使用的運算子混淆。例如:
DeliveryTruck t when t.GrossWeightClass switch { < 3000 => 10.00m - 2.00m, >= 3000 and <= 5000 => 10.00m, > 5000 => 10.00m + 5.00m, },
not的常見用法是將其應用於判空。例如:
not null => throw new ArgumentException($"Not a known vehicle type: {vehicle}", nameof(vehicle)), null => throw new ArgumentNullException(nameof(vehicle))
還有,if (!(e is Customer)) { ... }在新語法中,可以寫為if (e is not Customer) { ... }
5 目標型別
"Target typing"是當表示式從使用位置的上下文中獲取其型別時,我們使用的術語。C# 9.0支援新的型別推斷。
5.1 new
新語法中,如果是明確的型別,則在使用new時,可以不宣告型別了。比如:
Point p = new (3, 5);
5.2 ?? and ?:
目前,??與?:如果分支之間不是同一型別會報錯。新語法下,如果兩個分支都可以轉換為目標型別則是允許的:
Person person = student ?? customer; // Shared base type int? result = b ? 0 : null; // nullable value type
6 改進協變
有時,派生類中的方法返還比基類中的宣告更具體的型別是很有用的。C# 9.0 允許:
abstract class Animal { public abstract Food GetFood(); ... } class Tiger : Animal { public override Meat GetFood() => ...; }
此外,還要很多新的改進,讓我們拭目以待吧。