C#9.0主要特性的一些想法
前言
翻譯自 Mads Torgersen 2020年5月20日的博文《Welcome to C# 9.0》,Mads Torgersen 是微軟 C# 語言的首席設計師,也是微軟 .NET 團隊的項目群經(jīng)理。
C# 9.0 正在成形,我想和大家分享一下我們對下一版本語言中添加的一些主要特性的想法。
對于 C# 的每一個新版本,我們都在努力讓常見編碼場景的實現(xiàn)變得更加清晰和簡單,C# 9.0 也不例外。這次特別關注的是支持數(shù)據(jù)模型的簡潔和不可變表示。
就讓我們一探究竟吧!
一、僅初始化(init-only)屬性
對象初始化器非常棒。它們?yōu)轭愋偷目蛻舳颂峁┝艘环N非常靈活和可讀的格式來創(chuàng)建對象,并且特別適合于嵌套對象的創(chuàng)建,讓你可以一次性創(chuàng)建整個對象樹。這里有一個簡單的例子:
new Person { FirstName = "Scott", LastName = "Hunter" }
對象初始化器還使類型作者不必編寫大量的構造函數(shù)——他們所要做的就是編寫一些屬性!
public class Person { public string FirstName { get; set; } public string LastName { get; set; } }
目前最大的限制是屬性必須是可變的(即可寫的),對象初始化器才能工作:它們首先調(diào)用對象的構造函數(shù)(本例中是默認的無參數(shù)構造函數(shù)),然后賦值給屬性 setter。
僅初始化(init-only)屬性解決了這個問題!它引入了一個 init 訪問器,它是 set 訪問器的變體,只能在對象初始化時調(diào)用:
public class Person { public string FirstName { get; init; } public string LastName { get; init; } }
有了這個聲明,上面的客戶端代碼仍然是合法的,但是隨后對 FirstName 和 LastName 屬性的任何賦值都是錯誤的。
初始化(init) 訪問器和只讀(readonly)字段
因為 init 訪問器只能在初始化期間調(diào)用,所以允許它們更改封閉類的只讀(readonly)字段,就像在構造函數(shù)中一樣。
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))); } }
二、記錄(record)
譯者注:
原文中聲明一個記錄的 data class ** 聯(lián)合關鍵字現(xiàn)在已經(jīng)變成 record 關鍵字了,所以翻譯過程中做了修正。
如果您想使單個屬性不可變,那么僅初始化(init-only)屬性是極好的。如果您想要整個對象是不可變的,行為像一個值,那么你應該考慮聲明它為一個記錄(record):
public record Person { public string FirstName { get; init; } public string LastName { get; init; } }
對于記錄(record),賦予了它一些類似值的行為,我們將在下面深入探討。一般來說,記錄更應該被看作是“值”——數(shù)據(jù)(data),而不是對象!它們并不具有可變的封裝狀態(tài),相反,您需要通過創(chuàng)建表示新狀態(tài)的新記錄來表示其隨時間的變化。它們不是由它們的身份(identity)確定的,而是由它們的內(nèi)容確定的。
with 表達式
當使用不可變數(shù)據(jù)(data)時,一種常見的模式是從現(xiàn)有的值中創(chuàng)建新值來表示新狀態(tài)。例如,如果我們的 person 要更改他們的 LastName,我們會將其表示為一個新對象,該對象是舊對象的副本,只是有不同的 LastName。這種技巧通常被稱之為非破壞性突變(non-destructive mutation)。記錄(record)不是代表 person 在一段時間內(nèi)的 狀態(tài),而是代表 person 在給定時間點的 狀態(tài)。
為了幫助實現(xiàn)這種編程風格,記錄(record)允許使用一種新的表達式 —— with 表達式:
var otherPerson = person with { LastName = "Hanselman" };
with 表達式使用對象初始化器語法來聲明新對象與舊對象的不同之處。您可以指定多個屬性。
記錄(record)隱式定義了一個受保護的(protected)“復制構造函數(shù)”——一個接受現(xiàn)有記錄對象并逐字段將其復制到新記錄對象的構造函數(shù):
protected Person(Person original) { /* copy all the fields */ } // generated
with 表達式會調(diào)用“復制構造函數(shù)”,然后在上面應用對象初始化器來相應地變更屬性。
如果您不喜歡生成的“復制構造函數(shù)”的默認行為,您可以定義自己的“復制構造函數(shù)”,它將被 with 表達式捕獲。
基于值的相等(value-based equality)
所有對象都從對象類(object)繼承一個虛的 Equals(object) 方法。這被用作是當兩個參數(shù)都是非空(non-null)時,靜態(tài)方法 Object.Equals(object, object) 的基礎。
結(jié)構體重寫了 Equals(object) 方法,通過遞歸地在結(jié)構體的每一個字段上調(diào)用 Equals 來比較結(jié)構體的每一個字段,從而實現(xiàn)了“基于值的相等”。記錄(record)是一樣的。
這意味著,根據(jù)它們的“值性(value-ness)”,兩個記錄(record)對象可以彼此相等,而不是同一個對象。例如,如果我們將被修改 person 的 LastName 改回去:
var originalPerson = otherPerson with { LastName = "Hunter" };
現(xiàn)在我們將得到 ReferenceEquals(person, originalPerson) = false(它們不是同一個對象),但是 Equals(person, originalPerson) = true(它們有相同的值)。
如果您不喜歡生成的 Equals 重寫的默認逐個字段比較的行為,您可以自己編寫。您只需要注意理解“基于值的相等”是如何在記錄(record)中工作的,特別是在涉及繼承時,我們后面會講到。
除了基于值的 Equals 之外,還有一個基于值 GetHashCode() 的重寫。
數(shù)據(jù)成員(Data members)
絕大多數(shù)情況下,記錄(record)都是不可變的,僅初始化(init-only)公共屬性可以通過 with 表達式進行非破壞性修改。為了對這種常見情況進行優(yōu)化,記錄(record)更改了 string FirstName 這種形式的簡單成員聲明的默認含義,與其他類和結(jié)構體聲明中的隱式私有字段不同,它被當作是一個公共的、僅初始化(init-only) 自動屬性的簡寫!因此,聲明:
public record Person { string FirstName; string LastName; }
與我們之前的聲明意思完全一樣,即等同于聲明:
public record Person { public string FirstName { get; init; } public string LastName { get; init; } }
我們認為這有助于形成漂亮而清晰的記錄(record)聲明。如果您確實需要私有字段,只需顯式添加 private 修飾符:
private string firstName;
位置記錄(Positional records)
有時,對記錄(record)采用位置更明確的方法是有用的,其中它的內(nèi)容是通過構造函數(shù)參數(shù)提供的,并且可以通過位置解構來提取。
完全可以在記錄(record)中指定自己的構造函數(shù)和解構函數(shù):
public record 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); }
但是有一種更簡短的語法來表達完全相同的意思(參數(shù)名稱包裝模式modulo casing of parameter names):
public record Person(string FirstName, string LastName);
它聲明了公共的僅初始化(init-only)自動屬性以及構造函數(shù)和解構函數(shù),因此您就可以編寫:
var person = new Person("Scott", "Hunter"); // 用位置參數(shù)構造(positional construction) var (f, l) = person; // 用位置參數(shù)解構(positional deconstruction)
如果不喜歡生成的自動屬性,您可以定義自己的同名屬性,生成的構造函數(shù)和解構函數(shù)將只使用您自定義的屬性。
記錄與可變性(Records and mutation)
記錄(record)的基于值的語義不能很好地適應可變狀態(tài)。想象一下,將一個記錄(record)對象放入字典中。再次查找它依賴于 Equals 和 GetHashCode(有時)。但是如果記錄改變了狀態(tài),它的 Equals 值也會隨之改變,我們可能再也找不到它了!在哈希表實現(xiàn)中,它甚至可能破壞數(shù)據(jù)結(jié)構,因為位置是基于它的哈希碼得到的。
記錄(record)內(nèi)部的可變狀態(tài)或許有一些有效的高級用法,特別是對于緩存。但是重寫默認行為以忽略這種狀態(tài)所涉及的手工工作很可能是相當大的。
with 表達式和繼承(With-expressions and inheritance)
眾所周知,基于值的相等和非破壞性突變與繼承結(jié)合在一起時是極具挑戰(zhàn)性的。讓我們在運行示例中添加一個派生的記錄(record)類 Student:
public record Person { string FirstName; string LastName; } public record Student : Person { int ID; }
然后,讓我們從 with 表達式示例開始,實際地創(chuàng)建一個 Student,但將它存儲在 Person 變量中:
int newId = 1; Func<int> GetNewId = () => ++newId; //上面兩上是譯者在測試時發(fā)現(xiàn)需要添加的代碼。 Person person = new Student { FirstName = "Scott", LastName = "Hunter", ID = GetNewId() }; otherPerson = person with { LastName = "Hanselman" };
在最后一行帶 with 表達式的地方,編譯器不知道 person 實際上包含 Student。然而,如果新的 person(即 otherPerson) 不是一個真正的 Student 對象,并且具有從第一個 person 復制過去的相同的 ID,那么它就不是一個恰當?shù)目截悺?/p>
C# 實現(xiàn)了這一點。記錄(record)有一個隱藏的虛方法(virtual method),它被委托“克隆”整個對象。每個派生記錄類型都重寫此方法以調(diào)用該類型的復制構造函數(shù),并且派生記錄的復制構造函數(shù)將鏈接到基記錄的復制構造函數(shù)。with 表達式只需調(diào)用隱藏的“克隆”方法并將對象初始化器應用于其返回結(jié)果。
基于值的相等和繼承(Value-based equality and inheritance)
與 with 表達式支持類似,基于值的相等也必須是“虛的(virtual)”,即 Student 需要比較 Student 的所有字段,即使比較時靜態(tài)已知的類型是 Person 之類的基類型。這很容易通過重寫虛的(virtual) Equals 方法來實現(xiàn)。
然而,關于相等還有一個額外的挑戰(zhàn):如果你比較兩種不同的 Person 會怎樣?我們不能僅僅讓其中一個來決定實施哪個相等:相等應該是對稱的,所以不管兩個對象哪個在前面,結(jié)果應該是相同的。換句話說,它們必須在相等的實施上達成一致!
舉例說明一下這個問題:
Person person1 = new Person { FirstName = "Scott", LastName = "Hunter" }; Person person2 = new Student { FirstName = "Scott", LastName = "Hunter", ID = GetNewId() };
這兩個對象相等嗎? person1 可能會認為相等,因為 person2 對于 Person 的所有屬性都是正確的,但是 person2 不敢茍同!我們需要確保它們都同意它們是不同的對象。
同樣,C# 會自動為您處理這個問題。實現(xiàn)的方式是,記錄有一個名為 EqualityContract 的“虛的(virtual)”受保護的屬性。每個派生記錄(record)都會重寫它,為了比較相等,這兩個對象必須具有相同的 EqualityContract。
三、頂級程序(Top-level programs)
譯者注:
什么是 Top-level program ? 這是在頂級編寫程序的一種更簡單的方式:一個更簡單的 Program.cs 文件。
用 C# 編寫一個簡單的程序需要大量的樣板代碼:
using System; class Program { static void Main() { Console.WriteLine("Hello World!"); } }
這不僅對語言初學者來說是難以承受的,而且還會使代碼混亂,增加縮進級別。
在 C# 9.0 中,您可以選擇在頂級編寫你的主程序(main program):
using System; Console.WriteLine("Hello World!");
允許任何語句。此程序必須在文件中的 using 語句之后,任何類型或命名空間聲明之前執(zhí)行,并且只能在一個文件中執(zhí)行。就像目前只能有一個 Main 方法一樣。
如果您想返回一個狀態(tài)碼,您可以做。如果您想等待(await),您可以做。如果您想訪問命令行參數(shù),args 可以作為一個“魔法”參數(shù)使用。
局部函數(shù)是語句的一種形式,也允許在頂級程序中使用。從頂級語句部分之外的任何地方調(diào)用它們都是錯誤的。
四、改進的模式匹配(Improved pattern matching)
C# 9.0 中添加了幾種新的模式。讓我們從模式匹配教程(https://docs.microsoft.com/en-us/dotnet/csharp/tutorials/pattern-matching)的代碼片段的上下文中來看看它們:
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 _ 所示。但現(xiàn)在你只需寫下類型就可以了:
DeliveryTruck => 10.00m,
關系模式(Relational patterns)
C# 9.0 引入了與關系運算符 <、<= 等相對應的模式。因此,現(xiàn)在可以將上述模式的 DeliveryTruck 部分編寫為嵌套的 switch 表達式:
DeliveryTruck t when t.GrossWeightClass switch { > 5000 => 10.00m + 5.00m, < 3000 => 10.00m - 2.00m, _ => 10.00m, },
這里的 > 5000 和 < 3000 是關系模式。
邏輯模式(Logical patterns)
最后,您可以將模式與邏輯運算符 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 合并了兩個關系模式,形成一個表示區(qū)間的模式。
not 模式的一個常見用法是將其應用于 null 常量模式,如 not null。例如,我們可以根據(jù)未知實例是否為空來拆分它們的處理:
not null => throw new ArgumentException($"Not a known vehicle type: {vehicle}", nameof(vehicle)), null => throw new ArgumentNullException(nameof(vehicle))
此外,not 在 if 條件中包含 is 表達式時將會很方便,可以取代笨拙的雙括號,例如:
if (!(e is Customer)) { ... }
您可以寫成:
if (e is not Customer) { ... }
五、改進的目標類型(Improved target typing)
“目標類型(Target typing)”是一個術語,當一個表達式從使用它的地方的上下文中獲得其類型時,我們使用這個術語。例如,null 和 lambda表達式始終是目標類型的。
在 C# 9.0 中,一些以前不是目標類型的表達式變得可以由其上下文推導。
目標類型的 new 表達式(Target-typed new expressions)
C# 中的 new 表達式總是要求指定類型(隱式類型的數(shù)組表達式除外)?,F(xiàn)在,如果表達式被賦值為一個明確的類型,則可以省略該類型。
Point p = new (3, 5);
目標類型的 ?? 和 ?:(Target typed ?? and ?:)
有時有條件的 ?? 和 ?: 表達式在分支之間沒有明顯的共享類型,這種情況目前是失敗的。但是如果有一個兩個分支都可以轉(zhuǎn)換成的目標類型,在 C# 9.0 中將是允許的。
Person person = student ?? customer; // Shared base type int? result = b ? 0 : null; // nullable value type
六、協(xié)變式返回值(Covariant returns)
派生類中的方法重寫具有一個比基類型中的聲明更具體(更明確)的返回類型——有時這樣的表達是有用的。C# 9.0 允許:
abstract class Animal { public abstract Food GetFood(); ... } class Tiger : Animal { public override Meat GetFood() => ...; }
更多內(nèi)容……
要查看 C# 9.0 即將發(fā)布的全部特性并追隨它們的完成,最好的地方是 Roslyn(C#/VB 編譯器) GitHub 倉庫上的 Language Feature Status(https://github.com/dotnet/roslyn/blob/master/docs/Language%20Feature%20Status.md)。
總結(jié)
到此這篇關于C#9.0主要特性的一些想法的文章就介紹到這了,更多相關C#9.0主要特性內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
深入C#中使用SqlDbType.Xml類型參數(shù)的使用詳解
本篇文章是對在C#中使用SqlDbType.Xml類型參數(shù)的使用進行了詳細的分析介紹,需要的朋友參考下2013-05-05C#用websocket實現(xiàn)簡易聊天功能(服務端)
這篇文章主要為大家詳細介紹了C#用websocket實現(xiàn)簡易聊天功能,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-02-02C#實現(xiàn)json格式數(shù)據(jù)解析功能的方法詳解
這篇文章主要介紹了C#實現(xiàn)json格式數(shù)據(jù)解析功能的方法,結(jié)合實例形式較為詳細的分析了C#解析json格式數(shù)據(jù)的具體操作步驟與相關注意事項,需要的朋友可以參考下2017-12-12