原文:https://blog.csdn.net/csdnnews/article/details/106345959
微軟正在推進C# 9.0的開發,C# 9.0 將成為.NET 5 開發平台的一部分,預計於 11 月發布。微軟.NET團隊C#首席設計師Mads Torgersen表示,C# 9.0已初具規模,本文就分享下該語言下一版本中添加的一些主要功能。
C#的每個新版本都力求提升通用編程方面的清晰度與簡單性,C# 9.0也不例外,尤其注重支持數據形狀的簡潔與不可變表示。下面,我們就來詳細介紹!
一、僅可初始化的屬性
對象的初始化器非常了不起。它們為客戶端創建對象提供了一種非常靈活且易於閱讀的格式,而且特別適合嵌套對象的創建,我們可以通過嵌套對象一次性創建整個對象樹。下面是一個簡單的例子:
new Person
{
FirstName = "Scott",
LastName = "Hunter"
}
對象初始化器還可以讓程序員免於編寫大量類型的構造樣板代碼,他們只需編寫一些屬性即可!
public class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
}
目前的一大限制是,屬性必須是可變的,只有這樣對象初始化器才能起作用,因為它們需要首先調用對象的構造函數(在這種情況下調用的是默認的無參構造函數),然后分配給屬性設置器。
僅可初始化的屬性可以解決這個問題!它們引入了init訪問器。init訪問器是set訪問器的變體,它只能在對象初始化期間調用:
public class Person
{
public string FirstName { get; init; }
public string LastName { get; init; }
}
在這種聲明下,上述客戶端代碼仍然合法,但是后續如果你想為FirstName和LastName屬性賦值就會出錯。
01、初始化訪問器和只讀字段
由於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)));
}
}
二、記錄
如果你想保持某個屬性不變,那么僅可初始化的屬性非常有用。如果你希望整個對象都不可變,而且希望其行為宛如一個值,那么就應該考慮將其聲明為記錄:
public data class Person
{
public string FirstName { get; init; }
public string LastName { get; init; }
}
上述類聲明中的data關鍵字表明這是一個記錄,因此它具備了其他一些類似於值的行為,后面我們將深入討論。一般而言,我們更應該將記錄視為“值”(數據),而非對象。它們不具備可變的封裝狀態。相反,你可以通過創建表示新狀態的新記錄來表示隨着時間發生的變化。記錄不是由標識確定,而是由其內容確定。
01、With表達式
處理不可變數據時,一種常見的模式是利用現有的值創建新值以表示新狀態。例如,如果想修改某人的姓氏,那么我們會用一個新對象來表示,這個對象除了姓氏之外和舊對象完全一樣。通常我們稱該技術為非破壞性修改。記錄代表的不是某段時間的某個人,而是給定時間點上這個人的狀態。
為了幫助大家習慣這種編程風格,記錄允許使用一種新的表達方式:with表達式:
var otherPerson = person with { LastName = "Hanselman" };
with表達式使用對象初始化的語法來說明新對象與舊對象之間的區別。你可以指定多個屬性。
記錄隱式地定義了一個protected “復制構造函數”,這種構造函數利用現有的記錄對象,將字段逐個復制到新的記錄對象中:
protected Person(Person original) { /* copy all the fields */ } // generated
with表達式會調用復制構造函數,然后在其上應用對象初始化器,以相應地更改屬性。
如果你不喜歡自動生成的復制構造函數,那么也可以自己定義,with表達式就會調用自定義的復制構造函數。
02、基於值的相等性
所有對象都會從object類繼承一個虛的Equals(object)方法。在調用靜態方法Object.Equals(object, object)且兩個參數均不為null時,該Equals(object)就會被調用。
結構體可以重載這個方法,獲得“基於值的相等性”,即遞歸調用Equals來比較結構的每個字段。記錄也一樣。
這意味着,如果兩個記錄對象的值一致,則二者相等,但兩者不一定是同一對象。例如,如果我們再次修改前面那個人的姓氏:
var originalPerson = otherPerson with { LastName = "Hunter" };
現在,ReferenceEquals(person, originalPerson) = false(它們不是同一個對象),但Equals(person, originalPerson) = true (它們擁有相同的值)。
如果你不喜歡自動生成的Equals覆蓋默認的逐字段比較的行為,則可以編寫自己的Equals重載。你只需要確保你理解基於值的相等性在記錄中的工作原理,尤其是在涉及繼承的情況下,具體的內容我們稍后再做介紹。
除了基於值的Equals之外,還有一個基於值的GetHashCode()重載方法。
03、數據成員
在絕大多數情況下,記錄都是不可變的,它們的僅可初始化的屬性是公開的,可以通過with表達式進行非破壞性修改。為了優化這種最常見的情況,我們改變了記錄中類似於string FirstName這種成員聲明的默認含義。
在其他類和結構聲明中,這種聲明表示私有字段,但在記錄中,這相當於公開的、僅可初始化的自動屬性!因此,如下聲明:
public data class Person { string FirstName; string LastName; }
與之前提到過的下述聲明完全相同:
public data class Person
{
public string FirstName { get; init; }
public string LastName { get; init; }
}
我們認為這種方式可以讓記錄更加優美而清晰。如果你需要私有字段,則可以明確添加private修飾符:
private string firstName;
04、位置記錄
有時,用參數位置來聲明記錄會很有用,內容可以根據構造函數參數的位置來指定,並且可以通過位置解構來提取。
你完全可以在記錄中指定自己的構造函數和析構函數:
public data class 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);
}
但是,我們可以用更短的語法表達完全相同的內容(使用成員變量的大小寫方式來命名參數):
public data class Person(string FirstName, string LastName);
上述聲明了僅可初始化的公開的自動屬性以及構造函數和析構函數,因此你可以這樣寫:
var person = new Person("Scott", "Hunter"); // positional construction
var (f, l) = person; // positional deconstruction
如果你不喜歡生成的自動屬性,則可以定義自己的同名屬性,這樣生成的構造函數和析構函數就會自動使用自己定義的屬性。
05、記錄和修改
記錄的語義是基於值的,因此在可變的狀態中無法很好地使用。想象一下,如果我們將記錄對象放入字典,那么就只能通過Equals和GethashCode找到了。但是,如果記錄更改了狀態,那么在判斷相等時它代表的值也會發生改變!可能我們就找不到它了!在哈希表的實現中,這個性質甚至可能破壞數據結構,因為數據的存放位置是根據它“到達”哈希表時的哈希值決定的!
而且,記錄也可能有一些使用內部可變狀態的高級方法,這些方法完全是合理的,例如緩存。但是可以考慮通過手工重載默認的行為來忽略這些狀態。
06、with表達式與繼承
眾所周知,考慮繼承時基於值的相等性和非破壞性修改是一個難題。下面我們在示例中添加一個繼承的記錄類Student:
public data class Person { string FirstName; string LastName; }
public data class Student : Person { int ID; }
在如下with表達式的示例中,我們實際創建一個Student,然后將其存儲到Person變量中:
Person person = new Student { FirstName = "Scott", LastName = "Hunter", ID = GetNewId() };
otherPerson = person with { LastName = "Hanselman" };
在最后一行的with表達式中,編譯器並不知道person實際上包含一個Student。而且,即使otherPerson不是Student對象,它也不是合法的副本,因為它包含了與第一個對象相同的ID屬性。
C#解決了這個問題。記錄有一個隱藏的虛方法,能夠確保“克隆”整個對象。每個繼承的記錄類型都會通過重載這個方法來調用該類型的復制構造函數,而繼承記錄的復制構造函數會調用基類的復制構造函數。with表達式只需調用這個隱藏“clone”方法,然后在結果上應用對象初始化器即可。
07、基於值的相等性與繼承
與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可能會有不同的看法!我們需要確保二者都認同它們是不同的對象。
C#可以自動為你解決這個問題。具體的實現方式是:記錄擁有一個名為EqualityContract的受保護虛屬性。每個繼承的記錄都會重載這個屬性,而且為了比較相等,兩個對象必須具有相同的EqualityContract。
三、頂層程序
使用C#編寫一個簡單的程序需要大量的樣板代碼:
using System;
class Program
{
static void Main()
{
Console.WriteLine("Hello World!");
}
}
這不僅對初學者來說難度太高,而且代碼混亂,縮進級別也太多。
在C# 9.0中,你只需編寫頂層的主程序:
using System;
Console.WriteLine("Hello World!");
任何語句都可以。程序必須位於using之后,文件中的任何類型或名稱空間聲明之前,而且只能在一個文件中,就像只有一個Main方法一樣。
如果你想返回狀態代碼,則可以利用這種寫法。如果你想await,那么也可以這么寫。此外,如果你想訪問命令行參數,則args可作為“魔術”參數使用。
局部函數是語句的一種形式,而且也可以在頂層程序中使用。在頂層語句之外的任何地方調用局部函數都會報錯。
四、改進后的模式匹配
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))
};
01、簡單類型模式
當前,類型模式需要在類型匹配時聲明一個標識符,即使該標識符是表示放棄的_也可以,如上面的DeliveryTruck _。而如今你可以像下面這樣編寫類型:
DeliveryTruck => 10.00m,
02、關系模式
C# 9.0中引入了與關系運算符<、<=等相對應的模式。因此,你可以將上述模式的DeliveryTruck寫成嵌套的switch表達式:
DeliveryTruck t when t.GrossWeightClass switch
{
> 5000 => 10.00m + 5.00m,
< 3000 => 10.00m - 2.00m,
_ => 10.00m,
},
此處的 > 5000和< 3000是關系模式。
03、邏輯模式
最后,你還可以將模式與邏輯運算符(and、or和not)組合在一起,它們以英文單詞的形式出現,以避免與表達式中使用的運算符混淆。例如,上述嵌套的switch表達式可以按照升序寫成下面這樣:
DeliveryTruck t when t.GrossWeightClass switch
{
< 3000 => 10.00m - 2.00m,
>= 3000 and <= 5000 => 10.00m,
> 5000 => 10.00m + 5.00m,
},
中間一行通過and將兩個關系模式組合到一起,形成了表示間隔的模式。
not模式的常見用法也可應用於null常量模式,比如not null。例如,我們可以根據是否為null來拆分未知情況的處理方式:
not null => throw new ArgumentException($"Not a known vehicle type: {vehicle}", nameof(vehicle)),
null => throw new ArgumentNullException(nameof(vehicle))
此外,如果if條件中包含is表達式,那么使用not也很方便,可以避免笨拙的雙括號:
if (!(e is Customer)) { ... }
你可以這樣寫:
if (e is not Customer) { ... }
五、改進后的目標類型推斷
“目標類型推斷”指的是表達式從所在的上下文中獲取類型。例如,null和lambda表達式始終是目標類型推斷。
在C# 9.0中,有些以前不是目標類型推斷的表達式也可以通過上下文來判斷類型。
01、支持目標類型推斷的new表達式
C# 中的new表達式始終要求指定類型(隱式類型的數組表達式除外)。現在, 如果有明確的類型可以分配給表達式,則可以省去指定類型。
Point p = new (3, 5);
02、目標類型的??與?:
有時,條件判斷表達式中??與?:的各個分支之間並不是很明顯的同一種類型。現在這種情況會出錯,但在C# 9.0中,如果兩個分支都可以轉換為目標類型,就沒有問題:
Person person = student ?? customer; // Shared base type
int? result = b ? 0 : null; // nullable value type
03、協變的返回值
有時,我們需要表示出繼承類中重載的某個方法的返回類型要比基類中的類型更具體。C# 9.0允許以下寫法:
abstract class Animal
{
public abstract Food GetFood();
...
}
class Tiger : Animal
{
public override Meat GetFood() => ...;
}
六、更多內容
更多有關C# 9.0推出的新功能,請參照這個GitHub代碼庫(https://github.com/dotnet/roslyn/blob/master/docs/Language%20Feature%20Status.md)。
編程快樂!
參考鏈接:https://docs.microsoft.com/zh-cn/dotnet/csharp/whats-new/csharp-9