C # 9.0的record

官方消息: c # 9.0已通過時了!早在五月份,我就在博客中介紹了 c # 9.0計劃,下面是該文章的更新版本,以便與咱們最終發佈的計劃相匹配。web

對於每個新的 c # 版本,咱們都在努力提升常見編碼場景的清晰度和簡單性,c # 9.0也不例外。此次的一個特別重點是支持數據形狀的簡潔和不可變的表示。express

Init-only properties

對象初始化器很是棒。它們爲類型的客戶端建立對象提供了一種很是靈活和可讀的格式,特別適用於嵌套對象建立,在嵌套對象建立過程當中,能夠一次性建立整個對象樹。下面是一個簡單的例子:編程

var person = new Person { FirstName = "Mads", LastName = "Torgersen" };

對象初始值設定項還可使類型做者免於編寫大量構造樣板文件——他們所要作的就是編寫一些屬性!數組

public class Person
{
    public string? FirstName { get; set; }
    public string? LastName { get; set; }
}

目前的一個重大限制是,屬性必須是可變的,對象初始化器才能工做: 它們的工做方式是首先調用對象的構造函數(本例中是缺省的、無參數的構造函數) ,而後分配給屬性設置器。只有 init 屬性能夠解決這個問題!它們引入了一個 init 訪問器,這是 set 訪問器的一個變體,只能在對象初始化期間調用:ide

public class Person
{
    public string? FirstName { get; init; }
    public string? LastName { get; init; }
}

經過這個聲明,上面的客戶端代碼仍然是合法的,可是任何後續對 FirstName 和 LastName 屬性的賦值都是錯誤的:svg

var person = new Person { FirstName = "Mads", LastName = "Nielsen" }; // OK
person.LastName = "Torgersen"; // ERROR!

所以,只初始化屬性保護對象的狀態在初始化完成後不會發生變異。函數

Init accessors and readonly fields Init (訪問器和只讀字段)

由於只能在初始化期間調用 init 訪問器,因此容許它們改變封閉類的只讀字段,就像在構造函數中同樣。this

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)));
    }
}

Records

經典面向對象程序設計的核心思想是,對象具備強大的身份,並封裝了隨時間演變的可變狀態。C # 在這方面一直頗有效,可是有時候你想要的偏偏相反,這裏 c # 的默認設置每每會妨礙工做,讓事情變得很是艱難。編碼

若是你發現本身但願整個對象是不可變的,而且表現得像一個值,那麼你應該考慮將它聲明爲一個記錄:spa

public record Person
{
    public string? FirstName { get; init; }
    public string? LastName { get; init; }
}

記錄仍然是一個類,可是記錄關鍵字爲它灌輸了一些附加的類值行爲。通常來講,記錄是由它們的內容而不是它們的身份來定義的。在這方面,記錄更接近於結構,但記錄仍然是引用類型。

雖然記錄是可變的,可是它們主要是爲了更好地支持不可變數據模型而構建的。

With-expressions

在處理不可變數據時,一個常見的模式是從現有數據建立新的值來表示新的狀態。例如,若是咱們的人要改變他們的姓氏,咱們會將其表示爲一個新對象,這個對象是舊對象的副本,只是姓氏不一樣。這種技術一般被稱爲非破壞性突變。記錄表明的不是隨着時間的推移而表明的人,而是表明人在給定時間的狀態。爲了幫助這種編程風格,記錄容許一種新的表達式: with-expression:

var person = new Person { FirstName = "Mads", LastName = "Nielsen" };
var otherPerson = person with { LastName = "Torgersen" };

With-expressions 使用對象初始值設定項語法來講明新對象與舊對象的不一樣之處。能夠指定多個屬性。

With-expression 的工做方式是將舊對象的完整狀態複製到新對象中,而後根據對象初始值設定項對其進行變異。這意味着屬性必須有一個 init 或 set 訪問器才能在 with-表達式中更改。

Value-based equality

全部對象都從對象類繼承一個虛 Equals (對象)方法。這被用做 Object 的基礎。當兩個參數都非空時,Equals (object,object)靜態方法。結構會重寫這個函數,使其具備「基於值的相等性」,並經過遞歸地調用 Equals 對結構的每一個字段進行比較。唱片也是如此。這意味着,根據它們的「值性」,兩個記錄對象能夠相等而不是同一個對象。例如,若是咱們再次修改被修改人的姓氏:

咱們如今有 ReferenceEquals (person,originalPerson) = false (它們不是同一個對象) ,但 Equals (person,originalPerson) = true (它們具備相同的值)。除了基於價值的 Equals 以外,還有一個基於價值的 GetHashCode ()覆蓋。此外,記錄實現了 IEquatable < t > 並使 = = 和!= 操做符,所以基於價值的行爲在全部這些不一樣的平等機制中一致地顯示出來。

價值等同性和易變性並不總能很好地結合在一塊兒。一個問題是,更改值可能會致使 GetHashCode 的結果隨着時間的推移而更改,若是對象存儲在哈希表中,這將是不幸的!咱們不由止可變記錄,可是咱們不鼓勵它們,除非您已經考慮到了後果!

Inheritance (繼承)

記錄能夠從其餘記錄繼承:

public record Student : Person
{
    public int ID;
}

使用-表達式和值相等能夠很好地處理記錄繼承,由於它們考慮了整個運行時對象,而不只僅是靜態地知道它的類型。假設我建立了一個 Student,可是把它存儲在一個 Person 變量中:

Person student = new Student { FirstName = "Mads", LastName = "Nielsen", ID = 129 };

一個 with-expression 仍然會複製整個對象並保持運行時類型:

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, since ID's are different

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);

這聲明瞭公共 init-only auto-properties、構造函數和解構函數,以便您能夠編寫:

var person = new Person("Mads", "Torgersen"); // positional construction
var (f, l) = person;                        // positional deconstruction

若是不喜歡生成的 auto-property,能夠改成定義本身的同名屬性,生成的構造函數和解構函數將只使用該屬性。在這種狀況下,您可使用該參數進行初始化。好比說,你但願 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);

Top-level programs

用 c # 編寫一個簡單的程序須要大量的樣板代碼:

using System;
class Program
{
    static void Main()
    {
        Console.WriteLine("Hello World!");
    }
}

這不只對語言初學者來講是壓倒性的,並且會使代碼變得雜亂無章,增長縮進的級別。在 c # 9.0中,你只須要在頂層編寫你的主程序:

using System;

Console.WriteLine("Hello World!");

任何聲明都是容許的。程序必須在使用以後以及文件中的任何類型或名稱空間聲明以前執行,並且只能在一個文件中執行此操做,就像如今只能有一個 Main 方法同樣。若是您想返回狀態代碼,您能夠這樣作。若是你想等待,你能夠這樣作。若是您想訪問命令行參數,可使用 args 做爲「 magic」參數。

using static System.Console;
using System.Threading.Tasks;

WriteLine(args[0]);
await Task.Delay(1000);
return 0;

局部函數是一種語句形式,在頂級程序中也是容許的。從頂級語句部分之外的任何地方調用它們都是錯誤的。

Improved pattern matching

在 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))
    };

Simple type patterns

之前,當類型匹配時,類型模式須要聲明一個標識符——即便該標識符是一個丟棄的 _,如上面的 DeliveryTruck _ 中所示。可是如今你能夠只寫類型:

DeliveryTruck => 10.00m,

Relational patterns 關係模式

C # 9.0引入了與關係運算符 < 、 < = 等對應的模式。因此你如今能夠把上面模式的 DeliveryTruck 部分寫成一個嵌套的開關表達式:

DeliveryTruck t when t.GrossWeightClass switch
{
    > 5000 => 10.00m + 5.00m,
    < 3000 => 10.00m - 2.00m,
    _ => 10.00m,
},

這裏 > 5000和 < 3000是關係模式。

Logical patterns

最後,您能夠將模式與邏輯運算符組合起來,而且(或者)做爲單詞拼寫,以免與表達式中使用的運算符混淆。例如,上面的嵌套開關能夠按以下升序排列:

DeliveryTruck t when t.GrossWeightClass switch
{
    < 3000 => 10.00m - 2.00m,
    >= 3000 and <= 5000 => 10.00m,
    > 5000 => 10.00m + 5.00m,
},

這裏的中間格使用和結合兩個關係模式,並造成一個表示區間的模式。Not 模式的一個常見用法是將其應用於 null 常量模式,如 not null。例如,咱們能夠根據未知狀況是否爲空來分割處理:

not null => throw new ArgumentException($"Not a known vehicle type: {vehicle}", nameof(vehicle)),
null => throw new ArgumentNullException(nameof(vehicle))

在 if-conditions 中包含 is-expressions,而不是笨拙的雙括號,這樣也不方便:

if (!(e is Customer)) { ... }

你能夠直接說

if (e is not Customer) { ... }

事實上,在 is not 這樣的表達式中,咱們容許您爲 Customer 命名以供後續使用:

if (e is not Customer c) { throw ... } // if this branch throws or returns...
var n = c.FirstName; // ... c is definitely assigned here

Target-typed

「 Target typing」是一個術語,用於表達式從使用它的上下文中獲取其類型。例如,null 和 lambda 表達式老是目標類型的。

C # 中的新表達式老是要求指定一個類型(隱式類型數組表達式除外)。在 c # 9.0中,若是表達式被賦值爲一個明確的類型,則能夠省略該類型。

Point p = new (3, 5);

當你有不少重複的時候,好比在數組或者對象初始值設定項中,這個特別好:

Point[] ps = { new (1, 2), new (5, 2), new (5, -3), new (1, -3) };

Covariant returns

有時表示派生類中的重寫方法具備比基類中的聲明更具體的返回類型是有用的。9.0容許:

abstract class Animal
{
    public abstract Food GetFood();
    ...
}
class Tiger : Animal
{
    public override Meat GetFood() => ...;
}

還有更多…

查看完整的 c # 9.0特性的最佳位置是「 c # 9.0的新功能」文檔頁面。

相關文章
相關標籤/搜索