【譯】Welcome to C# 9.0

MeteorSeed發表於2020-06-01

  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() => ...;
}

   此外,還要很多新的改進,讓我們拭目以待吧。

原文連結

    https://devblogs.microsoft.com/dotnet/welcome-to-c-9-0/?utm_source=vs_developer_news&utm_medium=referral

相關文章