C#(99):C# 9.0 新特性( NET Framework 5.0 與 Visual Studio ? )
原文: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也不例外,尤其注重支援資料形狀的簡潔與不可變表示。下面,我們就來詳細介紹!
01、僅可初始化的屬性
物件的初始化器非常了不起。它們為客戶端建立物件提供了一種非常靈活且易於閱讀的格式,而且特別適合巢狀物件的建立,我們可以通過巢狀物件一次性建立整個物件樹。下面是一個簡單的例子:
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屬性賦值就會出錯。
02、初始化訪問器和只讀欄位
由於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))); } }
03、記錄
如果你想保持某個屬性不變,那麼僅可初始化的屬性非常有用。如果你希望整個物件都不可變,而且希望其行為宛如一個值,那麼就應該考慮將其宣告為記錄:
public data class Person
{
public string FirstName { get; init; }
public string LastName { get; init; }
}
上述類宣告中的data關鍵字表明這是一個記錄,因此它具備了其他一些類似於值的行為,後面我們將深入討論。一般而言,我們更應該將記錄視為“值”(資料),而非物件。它們不具備可變的封裝狀態。相反,你可以通過建立表示新狀態的新記錄來表示隨著時間發生的變化。記錄不是由標識確定,而是由其內容確定。
04、With表示式
處理不可變資料時,一種常見的模式是利用現有的值建立新值以表示新狀態。例如,如果想修改某人的姓氏,那麼我們會用一個新物件來表示,這個物件除了姓氏之外和舊物件完全一樣。通常我們稱該技術為非破壞性修改。記錄代表的不是某段時間的某個人,而是給定時間點上這個人的狀態。
為了幫助大家習慣這種程式設計風格,記錄允許使用一種新的表達方式:with表示式:
var otherPerson = person with { LastName = "Hanselman" };
with表示式使用物件初始化的語法來說明新物件與舊物件之間的區別。你可以指定多個屬性。
記錄隱式地定義了一個protected“複製建構函式”,這種建構函式利用現有的記錄物件,將欄位逐個複製到新的記錄物件中:
protected Person(Person original) { /* copy all the fields */ } // generated
with表示式會呼叫複製建構函式,然後在其上應用物件初始化器,以相應地更改屬性。
如果你不喜歡自動生成的複製建構函式,那麼也可以自己定義,with表示式就會呼叫自定義的複製建構函式。
05、基於值的相等性
所有物件都會從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()過載方法。
06、資料成員
在絕大多數情況下,記錄都是不可變的,它們的僅可初始化的屬性是公開的,可以通過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;
07、位置記錄
有時,用引數位置來宣告記錄會很有用,內容可以根據建構函式引數的位置來指定,並且可以通過位置解構來提取。
你完全可以在記錄中指定自己的建構函式和解構函式:
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
如果你不喜歡生成的自動屬性,則可以定義自己的同名屬性,這樣生成的建構函式和解構函式就會自動使用自己定義的屬性。
08、記錄和修改
記錄的語義是基於值的,因此在可變的狀態中無法很好地使用。想象一下,如果我們將記錄物件放入字典,那麼就只能通過Equals和GethashCode找到了。但是,如果記錄更改了狀態,那麼在判斷相等時它代表的值也會發生改變!可能我們就找不到它了!在雜湊表的實現中,這個性質甚至可能破壞資料結構,因為資料的存放位置是根據它“到達”雜湊表時的雜湊值決定的!
而且,記錄也可能有一些使用內部可變狀態的高階方法,這些方法完全是合理的,例如快取。但是可以考慮通過手工過載預設的行為來忽略這些狀態。
09、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”方法,然後在結果上應用物件初始化器即可。
10、基於值的相等性與繼承
與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。
11、頂層程式
使用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可作為“魔術”引數使用。
區域性函式是語句的一種形式,而且也可以在頂層程式中使用。在頂層語句之外的任何地方呼叫區域性函式都會報錯。
12、改進後的模式匹配
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))
};
13、簡單型別模式
當前,型別模式需要在型別匹配時宣告一個識別符號,即使該識別符號是表示放棄的_也可以,如上面的DeliveryTruck _。而如今你可以像下面這樣編寫型別:
DeliveryTruck => 10.00m,
14、關係模式
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是關係模式。
15、邏輯模式
最後,你還可以將模式與邏輯運算子(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) { ... }
16、改進後的目標型別推斷
“目標型別推斷”指的是表示式從所在的上下文中獲取型別。例如,null和lambda表示式始終是目標型別推斷。
在C# 9.0中,有些以前不是目標型別推斷的表示式也可以通過上下文來判斷型別。
17、支援目標型別推斷的new表示式
C# 中的new表示式始終要求指定型別(隱式型別的陣列表示式除外)。現在, 如果有明確的型別可以分配給表示式,則可以省去指定型別。
Point p = new (3, 5);
18、目標型別的??與?:
有時,條件判斷表示式中??與?:的各個分支之間並不是很明顯的同一種類型。現在這種情況會出錯,但在C# 9.0中,如果兩個分支都可以轉換為目標型別,就沒有問題:
Person person = student ?? customer; // Shared base type
int? result = b ? 0 : null; // nullable value type
19、協變的返回值
有時,我們需要表示出繼承類中過載的某個方法的返回型別要比基類中的型別更具體。C# 9.0允許以下寫法:
abstract class Animal
{
public abstract Food GetFood();
...
}
class Tiger : Animal
{
public override Meat GetFood() => ...;
}
20、更多內容
更多有關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