11月10日,C# 9.0已經正式發佈。一些新的特性也隨之而來,這個版本主要焦點放在了數據的簡潔性和不可變性表達上。編程
1. init關鍵字數組
1.1 僅初始化屬性 — init關鍵字ide
對象初始化方式對於建立對象來講是一種很是靈活和可讀的方式,特別對一口氣建立含有嵌套結構的樹型對象來講更有用。一個簡單的初始化例子以下:函數
var person = new Person { FirstName = "Mads", LastName = "Torgersen" };
原來要進行對象初始化,咱們不得不寫一些含有set訪問器的屬性,而且在構造函數的初次調用中,經過給屬性賦值來實現。spa
public class Person { public string? FirstName { get; set; } public string? LastName { get; set; } }
這種方式最大的侷限就是,對於初始化來講,屬性必須是可變的,也就是說,set訪問器對於初始化來講是必須的。而其餘狀況下又不須要set,所以這個setter就不合適了。爲了解決這個問題,僅僅只用來初始化的init訪問器出現了.。例如:命令行
public class Person { public string? FirstName { get; init; } public string? LastName { get; init; } }
init訪問器是一個只在對象初始化時用來賦值的set訪問器的變體,而且除過初始化進行賦值外,後續其餘的賦值操做是不容許的。上面定義的Person對象,在下面代碼中第一行初始化能夠,第二行再次賦值就不被容許了。 設計
var person = new Person { FirstName = "Mads", LastName = "Nielsen" }; // OK person.LastName = "Torgersen"; // ERROR!
所以,一旦初始化完成以後,僅初始化屬性就保護着對象免於改變code
1.2 init屬性訪問器和只讀字段 對象
由於init訪問器只能在初始化時被調用,因此在init屬性訪問器中能夠改變封閉類的只讀字段。blog
public class Person { private readonly string firstName = "<unknown>"; private readonly string lastName = "<unknown>"; 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 記錄 / Records
傳統面向對象的編程的核心思想是一個對象有着惟一標識,封裝着隨時可變的狀態。C#也是一直這樣設計和工做的。可是一些時候,你就很是須要恰好對立的方式。原來那種默認的方式每每會成爲阻力,使得事情變得費時費力。若是你發現你須要整個對象都是不可變的,且行爲像一個值,那麼你應當考慮將其聲明爲一個record類型。
public record Person { public string? FirstName { get; init; } public string? LastName { get; init; } }
一個record仍然是一個類,可是關鍵字record賦予這個類額外的幾個像值的行爲。一般說,records由他們的內容來界定,不是他們的標識。從這一點上講,records更接近於結構,可是他們依然是引用類型。
2.1 with表達式
當使用不可變的數據時,一個常見的模式是從現存的值建立新值來呈現一個新狀態。例如,若是Person打算改變他的姓氏(last name),咱們就須要經過拷貝原來數據,並賦予一個不一樣的last name值來呈現一個新Person。這種技術被稱爲非破壞性改變。做爲描繪隨時間變化的person,record呈現了一個特定時間的person的狀態。爲了幫助進行這種類型的編程,records就提出了一個新的表達式——with表達式:
var person = new Person { FirstName = "Mads", LastName = "Nielsen" }; var otherPerson = person with { LastName = "Torgersen" };
with表達式使用初始化語法來講明新對象在哪裏與原來對象不一樣。with表達式其實是拷貝原來對象的整個狀態值到新對象,而後根據對象初始化器來改變指定值。這意味着屬性必須有init或者set訪問器,才能用with表達式進行更改。
一個record隱式定義了一個帶有保護訪問級別的「拷貝構造函數」,用來將現有record對象的字段值拷貝到新對象對應字段中:
protected Person(Person original) { /* 拷貝全部字段 */ } // generated
with表達式就會引發拷貝構造函數被調用,而後應用對象初始化器來有限更改屬性相應值。若是你不喜歡默認的產生的拷貝構造函數,你能夠自定以,with表達式也會進行調用。
2.2 基於值的相等
全部對象都從object類型繼承了 Equals(object),這是靜態方法
Object.Equals(object, object)
用來比較兩個非空參數的基礎。
結構重寫了這個方法,經過遞歸調用每一個結構字段的Equals方法,從而有了「基於值的相等」,Recrods也是這樣。這意味着只要他們的值保持一致,兩個record對象能夠不是同一個對象就會相等。例如咱們將修改的Last name又修改回去了:
var originalPerson = otherPerson with { LastName = "Nielsen" };
如今咱們會獲得 ReferenceEquals(person, originalPerson)
= false (他們不是同一對象),可是 Equals(person, originalPerson)
= true (他們有一樣的值).。與基於值的Equals一塊兒的,還伴有基於值的GetHashCode()的重寫。另外,records實現了IEquatable<T>並重載了==和 !=這兩個操做符,以便於基於值的行爲在全部的不一樣的相等機制方面顯得一致。
基於值的相等和可變性不老是契合的很好。一個問題是改變值可能引發GetHashCode的結果隨時變化,若是這個對象被存放在哈希表中,就會出問題。咱們沒有不容許使用可變的record,可是咱們不鼓勵那樣作,除非你已經想到了後果。
若是你不喜歡默認Equals重寫的字段與字段比較行爲,你能夠進行重寫。你只須要認真理解基於值的相等時如何在records中工做原理,特別是涉及到繼承的時候,後面咱們會提到。
2.3 繼承 / Inheritance
記錄(record)能夠從其餘記錄(record)繼承:
public record Student : Person { public int ID; }
with表達式和值相等性與記錄的繼承結合的很好,由於他們考慮到了整個運行時對象,不僅是靜態的已知類型。好比,我建立一個Student對象,將其存在Person變量裏。
Person student = new Student { FirstName = "Mads", LastName = "Nielsen", ID = 129 };
with表達式仍然拷貝整個對象並保持着運行時的類型:
var otherStudent = student with { LastName = "Torgersen" }; WriteLine(otherStudent is Student); // true
一樣地,值相等性確保兩個對象有着一樣的運行時類型,而後比較他們的全部狀態:
Person similarStudent = new Student { FirstName = "Mads", LastName = "Nielsen", ID = 130 }; WriteLine(student != similarStudent); //true, 因爲ID值不一樣
2.4 位置記錄 / Positional records
有時,有更多的位置定位方式對一個記錄是頗有用的,在那裏,記錄的內容是經過構造函數的參數傳入,而且經過位置解構函數提取出來。你徹底可能會在記錄中定義你本身的構造和解構函數(注意不是析構函數)。以下所示:
public record Person { public string FirstName { get; init; } public string LastName { get; init; } public Person(string firstName, string lastName) => (FirstName, LastName) = (firstName, lastName); public void Deconstruct(out string firstName, out string lastName) => (firstName, lastName) = (FirstName, LastName); }
也能夠用更精簡的語法表達上面一樣的內容。
public record Person(string FirstName, string LastName);
該方式聲明瞭公開的、僅僅初始化的自動屬性、構造函數和解構函數,和2.1種第一行代碼帶有大括號的聲明方式不一樣。如今你就能夠寫以下代碼:
var person = new Person("Mads", "Torgersen"); // 位置構造函數 / positional construction var (f, l) = person; // 位置解構函數 / deconstruction
固然,若是你不喜歡產生的自動屬性,你能夠你本身自定義的同名屬性代替,產生的構造函數和解構函數將會只使用你自定義的那個。在這種狀況下,該參數處於你用於初始化的做用域內。例如,你想讓FirstName是個保護屬性:
public record Person(string FirstName, string LastName) { protected string FirstName { get; init; } = FirstName; }
一個位置記錄能夠像下面這樣調用父類構造函數。
public record Student(string FirstName, string LastName, int ID) : Person(FirstName, LastName);
3 頂層程序(Top-Level Programs)
一般,咱們寫一個簡單的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像魔法同樣也是可用的。
using static System.Console; using System.Threading.Tasks; WriteLine(args[0]); await Task.Delay(1000); return 0;
本地函數做爲語句的另外一種形式,也是容許在頂層程序代碼中使用的。在頂層代碼段外部的任何地方調用他們都會產生錯誤。
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)) };
(1)簡單類型模式
當前,進行類型匹配的時候,一個類型模式須要聲明一個標識符——即便這標識符是一個棄元_,像上面代碼中的DeliveryTruck _
。可是在C#9.0中,你能夠只寫類型,以下所示:
DeliveryTruck => 10.00m,
(2)關係模式
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就是關係模式。
(3)邏輯模式
最後,你能夠用邏輯操做符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。例如咱們要根據是否爲空來把一個未知分支的處理進行拆分:
not null => throw new ArgumentException($"Not a known vehicle type: {vehicle}", nameof(vehicle)), null => throw new ArgumentNullException(nameof(vehicle))
在包含了is表達式的if條件語句中,用於取代笨拙的雙括號,使用not也會很方便:
if (!(e is Customer)) { ... }
你能夠這樣寫:
if (e is not Customer) { ... }
實際上,在is not表達式裏,容許你給Customer指定名稱,以便後續使用。
if (e is not Customer c) { throw ... } // 若是這個分支拋出異常或者返回... var n = c.FirstName; // ... 這裏,c確定已經被賦值了,不會爲空
5 類型推導new表達式
類型推導是從一個表達式所在的位置根據上下文得到它的類型時使用的一個術語。例如null和lambda表達式老是涉及到類型推導的。
在C#中,new表達式老是要求一個具體指定的類型(除了隱式類型數組表達式)。如今,若是表達式被指派給一個明確的類型時,你能夠忽略new關鍵字後面的類型。
Point p = new (3, 5);
當有大量重複,這個特別有用。例以下面數組初始化:
Point[] ps = { new (1, 2), new (5, 2), new (5, -3), new (1, -3) };
6 返回值類型支持協變
有時候,在子類的一個重寫方法中返回一個更具體的、且不一樣於父類方法的返回類型更爲有用,C# 9.0對這種狀況提供了支持。以下列子中,子類Tiger的在重寫父類Animal的GetFood方法時,返回值使用了Meat而不是Food,就更爲形象具體。
abstract class Animal { public abstract Food GetFood(); ... } class Tiger : Animal { public override Meat GetFood() => ...; }