C# 10 特性一覽
Part 1 結(jié)構(gòu)記錄類型
C# 9 有一個(gè)新的數(shù)據(jù)類型,叫做記錄(Record)。這個(gè)類型是一種特殊的引用類型,我們只需要給出一個(gè)東西的具體屬性,就可以自動(dòng)為這個(gè)類型生成指定的比較器(Equals
方法、比較運(yùn)算符 operator ==
和 operator !=
、GetHashCode
方法,甚至是 ToString
方法等等)。
舉個(gè)例子,我們可以這么寫:
這就等價(jià)于一個(gè)類 Point
,然后生成 X
和 Y
屬性,以及相關(guān)的方法:
這樣的東西。你看看,就寫一句話就能生成一系列的內(nèi)容,是不是很方便。
不過,Point
這樣就好比把前文的 sealed class
改寫成 struct
record
或者 record class
來表示一個(gè)類的記錄類型。
前文用到的
init
屬性是 C# 9 里誕生的、用來表示屬性只在初始化器和構(gòu)造器里才可賦值的一種屬性賦值模型。它比set
的使用范圍要少,set
隨時(shí)隨地都可以賦值(最多加一個(gè)訪問修飾符,但并不能阻止內(nèi)部的任何時(shí)候的賦值)。init
僅允許初始化器和構(gòu)造器里使用賦值,其它任何地方都不能賦值。這樣做就避免了很多地方的安全問題,同時(shí)也提供了一種語法上的約束。
Part 2 全局 using
指令
using
指令。
你只需要在單獨(dú)的文件里寫上這么一句話:
global using IntegerList = System.Collections.Generic<int>;
那么,整個(gè)項(xiàng)目就都可以使用這個(gè) IntegerList
了。
Part 3 namespace
指令
在 C# 10 里,你可以使用一句類似 using
指令的形式,對(duì)文件整體進(jìn)行聲明命名空間,這樣的話,后續(xù)的內(nèi)容就不用再用大括號(hào)了:
這樣就等價(jià)于經(jīng)典寫法:
namespace
指令后,就不可在文件里多次聲明嵌套或并行的命名空間了。換句話說,這種寫法僅僅是一個(gè)語法糖。
Part 4 內(nèi)插字符串?dāng)U展
該特性實(shí)際上有兩個(gè)小的子特性。下面我們來說下。
Part 4-1 推廣到集合類型的 params
參數(shù)
在早期的 C#,params
參數(shù)后的參數(shù)類型必須是數(shù)組。由于數(shù)組本身比較笨重的緣故(是引用類型),所以就很不利于擴(kuò)展性能。因此,C# 10 開始允許用戶使用 params 集合
的模式來聲明參數(shù)。比如說
如例子所示,我們?cè)试S參數(shù)使用 params Span<string>
目前,C# 團(tuán)隊(duì)打算允許如下一些集合類型作為 params
的依托:
params T[]
(經(jīng)典寫法)params IEnumerable<T>
params Span<T>
params ReadOnlySpan<T>
另外,按照次序,如果有這樣四個(gè)方法的重載的話,C# 會(huì)優(yōu)先采用效率高的 ReadOnlySpan<T>
類型作為調(diào)用方,然后是 Span<T>
、T[]
,最后才是 IEnumerable<T>
。
其實(shí)也很好記。
ReadOnlySpan<T>
和Span<T>
一定是效率最高的,因?yàn)樗鼈儍H存儲(chǔ)在棧內(nèi)存里;但是,ReadOnlySpan<T>
不可變,所以相較于后者來說,它更為安全;接著,T[]
和IEnumerable<T>
里,顯然是數(shù)組更好用,然后才是迭代集合IEnumerable<T>
,因?yàn)榈弦褂煤軓?fù)雜的迭代器模式來迭代,是一種耗費(fèi)性能較高的類型;但為了可讀性,C# 依舊允許了這一個(gè)類型作為params
參數(shù)。
Part 4-2 更高效、不裝箱拆箱的內(nèi)插字符串
在允許了前文給的特性后,內(nèi)插字符串就可以使用更為高級(jí)的語法,來避免裝箱和拆箱了。在 C# 6 里,內(nèi)插字符串會(huì)被自動(dòng)翻譯為 string.Format
的調(diào)用。而很遺憾的是,這個(gè)方法的參數(shù)都是 object
類型的可變參數(shù)序列,因而值類型在傳入的時(shí)候必然導(dǎo)致裝箱,損失了性能。在允許了 4-1 特性后,string.Format
就有了新的實(shí)現(xiàn)方式:
這里的 Variant
類型在 C# 團(tuán)隊(duì)實(shí)現(xiàn)后才會(huì)給出。這里你可以理解成一個(gè)“任何類型都可以兼容的值類型”。換句話說,任何類型都可以轉(zhuǎn)這個(gè)類型來存儲(chǔ),類似 object
,只是這里用 Variant
表達(dá)出來可以避免裝箱(因?yàn)槭侵殿愋停?;至于引用類型,傳過來就會(huì)被改成地址什么的,總之不會(huì)裝箱就對(duì)了。
其它的問題和解答,你可以查看(英文原文檔)。
Part 5 無參值類型構(gòu)造器
C# 一直以來就存在一個(gè)備受爭(zhēng)議的語法點(diǎn):值類型的無參構(gòu)造器到底是否有必要禁止用戶自己聲明。C# 團(tuán)隊(duì)是這么想的:由于值類型是作為基本數(shù)據(jù)類型的實(shí)現(xiàn)體現(xiàn)(比如 int
這些數(shù)據(jù)類型),那么它們必然就有賦值和構(gòu)造的默認(rèn)權(quán)力。如果給用戶用的話,用戶就有可能會(huì)誤用無參構(gòu)造器,導(dǎo)致初始化的問題。因此,C# 的引用類型和值類型的無參構(gòu)造器表現(xiàn)的行為不同:值類型的是永久都存在的,且你無法自己定義;而引用類型的無參構(gòu)造器在沒有構(gòu)造器的類里會(huì)自動(dòng)生成,有構(gòu)造器的話,就不會(huì)自動(dòng)生成,需要你自己定義。
那么,C# 團(tuán)隊(duì)考慮了很久,終于想通了。用戶開始可以自己定義無參構(gòu)造器了。
Part 5-1 真·無參構(gòu)造器
如代碼所示。
這個(gè)例子展示了無參構(gòu)造器的使用方式。顯然,跟引用類型是基本一樣的,只是多了一點(diǎn):如果你不定義的話,默認(rèn)會(huì)生成一個(gè)無參構(gòu)造器(S0
結(jié)構(gòu),y
會(huì)被自動(dòng)賦值默認(rèn)數(shù)值 default(object)
,也就是 null
);但一經(jīng)定義,就必須給所有沒有賦值的字段和屬性賦值。
當(dāng)然,此時(shí)的構(gòu)造器既然允許自定義了,這樣就使得構(gòu)造器可以定義和修改訪問修飾符了。如果訪問修飾符設(shè)置為 private
的話,那么外部就無法使用 new
來實(shí)例化該類型的對(duì)象了。這一點(diǎn)和引用類型還是一樣的。
Part 5-2 結(jié)構(gòu)的字段初始化器
可以看到,這個(gè)特性一旦出現(xiàn),就相當(dāng)于誕生了另外一個(gè)特性:結(jié)構(gòu)的字段初始化器。C# 早期同樣是不允許你給字段設(shè)置默認(rèn)數(shù)值的;相反,你必須在構(gòu)造器里賦值,還不能是默認(rèn)的無參構(gòu)造器里。
Part 5-3 default(T)
表達(dá)式
另外,由于值類型和引用類型的默認(rèn)數(shù)值不同的關(guān)系,定義了無參構(gòu)造器必然會(huì)影響到它的默認(rèn)數(shù)值 default(T)
表達(dá)式。實(shí)際上真的是這樣嗎?并不是。還是拿 Point
類型舉例。即使你給出了默認(rèn)構(gòu)造器的調(diào)用,default(Point)
依舊還是原始數(shù)據(jù)的原始數(shù)據(jù)類型的默認(rèn)數(shù)值構(gòu)造成的實(shí)例的結(jié)果。
那么,default(Point)
還是 Point { X = 0, Y = 0 }
,而不是 Point { X = -2147483647, Y = -2147483647 }
我們把“原始數(shù)據(jù)的原始數(shù)據(jù)類型的默認(rèn)數(shù)值構(gòu)造成的實(shí)例”叫做零初始化實(shí)例(Zeroed Instance),那么,default(T)
的定義就可以縮減為“該類型的零初始化實(shí)例”;換句話說,該類型的零初始化實(shí)例就是這個(gè)值類型的默認(rèn)數(shù)值。那么,使用 default
表達(dá)式的時(shí)候,就算你定義了無參構(gòu)造器,編譯器也會(huì)始終忽略它。
Part 6 參數(shù)的 nameof
表達(dá)式
我們經(jīng)常會(huì)考慮到這樣的使用場(chǎng)合:
由于特性在方法外部,因此無法直接使用 obj
這個(gè)名稱。傳入字符串有時(shí)候依然不方便,因?yàn)槟阈薷牧?obj
的名字之后,上方特性傳入的 "obj"
這樣就很方便了。
Part 7 增強(qiáng)對(duì)是否為 null
的對(duì)象的代碼分析
C# 8 誕生了可空引用類型的概念,并提供了基本的分析模型。但很遺憾的是,很多時(shí)候,編譯器依舊無法識(shí)別對(duì)象已經(jīng)不可能是 null
的情況,進(jìn)而產(chǎn)生語義分析上的 bug。C# 10 提出了增強(qiáng)分析的概念,這樣的話,很多原本是 bug 的情況就得到了解決。舉個(gè)例子。
比如這個(gè)例子下,c
不可空后,調(diào)用 M
方法后,如果返回值為 true
的話,參數(shù) obj1
就不能為空了。而編譯器暫時(shí)無法識(shí)別這種變量的傳遞(最開始是從 c != null
開始的),因此分析這個(gè)地方的時(shí)候,obj1
仍然不知道是不是為 null
。
C# 10 會(huì)對(duì)這樣類似的場(chǎng)景的分析進(jìn)行修復(fù)。
順帶凡爾賽一波,我提了一個(gè)屬于這個(gè)特性主題的 issue,它們也放在了這個(gè)提案里作為解決對(duì)象。
Part 8 ref
和 partial
關(guān)鍵字的順序
在誕生了 ref struct
這種類型之后,C# 由于沒有考慮到語法的靈活性,因此如果 ref struct
是分部類型的話,就必須寫成 ref partial struct
,而調(diào)轉(zhuǎn)順序 partial ref struct
則是錯(cuò)誤的寫法。
C# 10 將對(duì)這個(gè)問題進(jìn)行修復(fù)。
Part 9 參數(shù)可空性驗(yàn)證
!!
如果一個(gè)參數(shù)為 null
,我們期望使用 throw
語句來產(chǎn)生異常信息。于是,代碼大概就長(zhǎng)這樣:
str
參數(shù)聲明上,這樣的話,if
Part 9-2 對(duì)于值類型和可空引用類型的可空校驗(yàn)
當(dāng)然,這個(gè)特性用在如果你沒有對(duì)項(xiàng)目啟用 nullable enable
的地方。因?yàn)槲覀兡馨汛a寫成 string? arg!!
就沒有必要為類型追加這個(gè)冗余的可空標(biāo)記符號(hào) ?
。既然對(duì)象一旦為空就拋異常,那么我們何必寫成 string?
呢?寫成 string
不好嗎?你說是吧。
另外,值類型本身就不可能為 null
了,我們何從談起對(duì)值類型使用 !!
呢?當(dāng)然了,Nullable<T>
除外。不過,對(duì)可空值類型使用 !!
的話,還不如不用可空值類型,直接不讓它可空不就行了。
所以,總的來說,只要是對(duì)可空類型(包含值類型、引用類型這兩種)使用 !!
的話,編譯器都會(huì)產(chǎn)生警告,然后提示你“沒必要這么用”。
Part 10 調(diào)用方表達(dá)式特性
在 C# 5 的時(shí)候,誕生了調(diào)用方參數(shù)特性。我們可以使用可選參數(shù)配合 CallerMemberNameAttribute
等特性標(biāo)記到參數(shù)上面,這樣的話,運(yùn)行時(shí)就會(huì)自動(dòng)把該數(shù)據(jù)傳入到這個(gè)參數(shù)上去(比如 CallerMemberNameAttribute
會(huì)把調(diào)用方方法名傳過來)。
現(xiàn)在,C# 10 里新增一個(gè)調(diào)用方表達(dá)式特性,這樣的話,就可以把參數(shù)上的表達(dá)式傳過來了。
Part 11 泛型特性
我們目前用到了很多需要傳入一個(gè)或多個(gè) Type
C# 10 里允許泛型特性的支持,我們就可以使用這樣的語法傳入一個(gè)泛型,帶一個(gè)類型的方式,就不必再考慮這種性能問題了。
這樣就比起經(jīng)典寫法 [DebuggerProxy(typeof(ComplexValueDebuggerProxy))]
要好看不少。而且,特性里的屬性就可以直接使用泛型參數(shù)替換 object
來表達(dá)了,確實(shí)很方便。
Part 12 解構(gòu) 表達(dá)式
如果我們有這么一句話在 C# 里是允許的話:
你覺得,arg1
和 arg2
會(huì)是多少呢?C# 10 里將允許這個(gè)寫法,這樣的話,arg1
和 arg2
就會(huì)默認(rèn)賦值 0。
Part 13 內(nèi)插字符串常量
C# 6 誕生的內(nèi)插字符串并不能直接當(dāng)成常量使用。舉個(gè)例子:
B
C# 10 開始允許這一點(diǎn),內(nèi)插字符串也可以認(rèn)為是常量了;當(dāng)然前提是,內(nèi)插的部分也都得是常量才行;而且內(nèi)插的對(duì)象必須也得是 string
才行。
Part 14 元組表達(dá)式里內(nèi)聯(lián)變量定義
這句話不好理解。舉個(gè)例子。
假如,name
是本身就有的東西(它可能是屬性,或者是字段,或者是臨時(shí)變量),而 age
student
變量解構(gòu)了的話,由于 age
的定義變量語句無法寫到賦值語句里面去,所以只能分開成兩行書寫。
C# 10 將允許你內(nèi)嵌定義語句到值元組賦值的語句里去。
這樣就合二為一了。name
照舊賦值,而 age
Part 15 模式匹配 IV:集合模式
從 C# 7 開始,模式匹配就是一種特別高大上的語法,搞得別的編程語言紛紛效仿。C# 7 里允許了在 switch
語句里使用 when
從句,并同時(shí)允許了 is
表達(dá)式里內(nèi)聯(lián)變量定義;C# 8 里允許了遞歸模式匹配;C# 9 里則又多了 and
、not
和 or
的邏輯模式匹配。C# 10 這次帶來的是集合模式匹配。
集合模式匹配可以對(duì)集合的元素進(jìn)行解構(gòu)、處理和判別。
Part 15-1 長(zhǎng)度模式
長(zhǎng)度模式聽起來好像是在判斷集合的長(zhǎng)度,但是這一點(diǎn)不是可以用 Length
屬性的遞歸模式作為判斷對(duì)象嗎?是的,長(zhǎng)度模式并不是這個(gè)意思。
長(zhǎng)度模式用中括號(hào)來獲取數(shù)據(jù)元素,然后通過冒號(hào)和模式來表達(dá)一個(gè)對(duì)象的指定索引位置上的數(shù)據(jù)是不是滿足這個(gè)模式。舉個(gè)例子:
當(dāng) arr
必須是帶 Length
或 Count
屬性的類型,且擁有一個(gè)以 int
arr
數(shù)據(jù)的第 3 個(gè)元素是一個(gè)整數(shù),且不是 -1 的時(shí)候,滿足條件。
如果 arr
在這里不能確定和斷定是不是包含 Length
或 Count
屬性,或者是 int
類型作參數(shù)的索引器的時(shí)候,那么編譯器就會(huì)告訴你,arr
由于無法斷定類型,所以無法使用該模式來對(duì)數(shù)據(jù)進(jìn)行校驗(yàn)。
上方語法等價(jià)于 arr[2] is int val && val != -1
。你可能不一定能理解,為什么 arr[2] is int val
這句話要寫出來。因?yàn)?arr
的元素類型尚不清楚,換句話說就是,arr
可能是一個(gè) object?[]
,所以無法直接取數(shù)據(jù);而使用 ==
會(huì)引起數(shù)據(jù)比較的錯(cuò)誤:因?yàn)樽髠?cè)是 object
類型,右側(cè)是 int
類型,等號(hào)就會(huì)認(rèn)為是倆 object
的引用比較,所以是錯(cuò)誤的用法。
總結(jié)一下。只要類型:
有一個(gè)
this[int]
的索引器,且必須有get
語句;有一個(gè)叫做
Length
或者是Count
的屬性。
那么類型就可以使用長(zhǎng)度模式對(duì)具體某個(gè)元素作匹配。
Part 15-2 集合模式
長(zhǎng)度模式我們說完了,接下來說一下集合模式。當(dāng)長(zhǎng)度模式要連續(xù)拼接多個(gè)元素判斷的時(shí)候,長(zhǎng)度模式就顯得很麻煩了。那么,集合模式就出現(xiàn)了。
obj
的類型就開始判斷對(duì)象的數(shù)據(jù)了,顯然是很麻煩的。C# 10 提供了一種輕快的語法:
我們來說一下,這個(gè)寫法是啥意思。首先,{ 1, 2, 3, .. }
..
在 C# 8 里就有,它表示取序列的一部分。在模式匹配里(就是這里),這個(gè)記號(hào)表達(dá)的是“后面還有別的數(shù)據(jù),不過我們不作驗(yàn)證了”。換句話說,{ 1, 2, 3 }
和 { 1, 2, 3, .. }
是兩個(gè)不同的意思:前者表示必須序列只有三個(gè)元素,且必須依次是 1、2、3;而后者則可表示元素至少有三個(gè),只要前三個(gè)順次是 1、2、3 就可以了;后面不管是啥都行。
啰嗦一下。由于
int[]
和{ 1, 2, 3, .. }
這兩個(gè)模式是分開的兩個(gè)匹配邏輯(一個(gè)是類型判斷,一個(gè)是數(shù)據(jù)判斷),且它們是且的關(guān)系,因此按道理來說,是可以加and
在中間的:is int[] and { 1, 2, 3, .. }
。實(shí)際上可以嗎?可以的。但是沒有必要,因?yàn)?C# 知道這里是兩個(gè)判斷的關(guān)系。所以這里的int[] { 1, 2, 3, .. }
雖然看起來有點(diǎn)像數(shù)組的初始化器,但編譯器是知道這里不是在初始化數(shù)組,而是一個(gè)判斷的兩個(gè)條件(一個(gè)判斷類型的條件和一個(gè)判斷數(shù)據(jù)的條件)。
不過,啥樣的數(shù)據(jù)類型可以使用和校驗(yàn)?zāi)兀考热粚?duì)象可以解析數(shù)據(jù),那么前面要滿足長(zhǎng)度模式的要求必須都得滿足。因此:
有一個(gè)
this[int]
的索引器,且必須有get
語句;有一個(gè)叫做
Length
或者是Count
的屬性。
依然是這樣的條件。
Part 15-3 切片模式
那么,集合貌似有了判斷模式了,好像差不多可以結(jié)束了。C# 10 還提供了一種新鮮的語法,除了以數(shù)據(jù)為單位判斷,還可以以數(shù)據(jù)序列作為切片。
arr[0..10]
,這表示把 arr
的前 10 個(gè)元素取出來,而這個(gè)語法叫切片(Slice)。只要有一個(gè)方法 Slice(int, int)
定義切片的邏輯,那么對(duì)象就可以切片了。C# 10 里允許我們使用切片到模式匹配里。舉個(gè)例子,expr is { 1, .. var s, 3 }
就表示我們對(duì)中間的序列作切片,然后切片結(jié)果用 s
變量表示,因此,這個(gè)寫法等價(jià)于 expr.Length >= 2 && expr[0] == 1 && expr[^1] == 3 && expr[1..^1] is var s
。特別要注意的是,這里第一個(gè)條件并不是直接數(shù)值判斷,而是長(zhǎng)度判斷:expr.Length >= 2
。這是因?yàn)楹罄m(xù)的條件無法保證對(duì)象的長(zhǎng)度是多少,貿(mào)然取值會(huì)導(dǎo)致 IndexOutOfRangeException
的異常。
那么,給一些例子給你看看。
棄元(Discard)語法。我們不在意這里的數(shù)據(jù)是多少,但必須要占位來表示這里是集合的第幾個(gè)元素,就使用棄元來表達(dá)(后兩種情況就用到了占位這個(gè)概念)。
另請(qǐng)注意,這里
expr
是不知道啥類型的,所以可能集合內(nèi)的元素都是object
。因此在注釋里用的是is
而不是==
當(dāng)然了,如果只有一個(gè)切片的范圍記號(hào) ..
,而不判斷數(shù)據(jù)的話(即 { .. }
),就等價(jià)于 obj.Length >= 0
這個(gè)條件,或者 obj is { Length: >= 0 }
。另外,后者這個(gè)寫法還比前者多判斷一下 null
:obj is not null and { Length: >= 0 }
;而前者只判斷 Length
屬性是不是至少為 0。
我們來總結(jié)一下 C# 7 開始允許的所有模式匹配:
C# 7
expr is T value
(增強(qiáng)is
模式)expr is var variable
(var
模式)expr is var (value1, value2, value3)
(解構(gòu)模式)expr is var (_, _, value3, _)
(棄元模式)C# 8
expr is { Property1: value1, Property2: { InnerProperty: value2 } }
(遞歸模式)expr is { } notNullResult
(空遞歸模式/不可空校驗(yàn)?zāi)J剑?/span>C# 9
expr is var value and not (value1 or value2)
(邏輯模式)C# 10
expr is { [index]: value }
(長(zhǎng)度模式)expr is { value1, value2, _, .., value3, .., value4, _ }
(集合模式)expr is { _, _, .. var slice, _, _ }
(切片模式)
當(dāng)我們需要混用的時(shí)候,需要注意一下要求。由于很多的模式匹配上都是用大括號(hào)來表達(dá)和標(biāo)記模式匹配的范圍和界限,因此如果混用可能就導(dǎo)致語法不清晰。因此,C# 10 只能讓我們把集合和切片模式寫在整體的最后面;而前面則是 C# 7 到 9 里的基本模式匹配。
這樣的寫法。