最美情侣中文字幕电影,在线麻豆精品传媒,在线网站高清黄,久久黄色视频

歡迎光臨散文網(wǎng) 會(huì)員登陸 & 注冊(cè)

探索 C# 10 的記錄結(jié)構(gòu)類(lèi)型

2021-09-25 05:08 作者:SunnieShine  | 我要投稿

C# 9 帶來(lái)的記錄類(lèi)型確實(shí)給我們帶來(lái)了很多方便的地方,讓我們實(shí)現(xiàn)一個(gè) POCO 變得相當(dāng)容易。不過(guò)問(wèn)題在于,一個(gè)記錄類(lèi)型還是太笨重了。且不說(shuō)生成的東西和成員,記錄類(lèi)型是一個(gè)類(lèi),所以實(shí)例化一個(gè)類(lèi)的話(huà),必須走堆內(nèi)存一遭,所以耗時(shí)必然會(huì)很多。

為了解決這個(gè)問(wèn)題,C# 10 帶來(lái)了結(jié)構(gòu)記錄類(lèi)型。

還是先來(lái)說(shuō)一下基本語(yǔ)法

為了體現(xiàn)結(jié)構(gòu)記錄類(lèi)型的本質(zhì)是一個(gè)結(jié)構(gòu),我們需要在 record 上下文關(guān)鍵字的后面補(bǔ)充一個(gè) struct 關(guān)鍵字。假設(shè)我現(xiàn)在擁有一個(gè)這樣的數(shù)據(jù)類(lèi)型,那么簡(jiǎn)化后是這樣的:

我們之前是把 class 改成 record,而現(xiàn)在我們把 struct 改成 record struct。我們就把這個(gè)用 record struct 組合關(guān)鍵字修飾的類(lèi)型稱(chēng)為結(jié)構(gòu)記錄類(lèi)型,或者記錄結(jié)構(gòu)類(lèi)型(Record Struct);因?yàn)?C# 10 誕生了結(jié)構(gòu)版本的記錄類(lèi)型,所以為了方便比較和對(duì)比,原本 C# 9 里的記錄類(lèi)型我們這里也可以改名叫做類(lèi)記錄類(lèi)型,或者記錄類(lèi)類(lèi)型(Record Class)。稍微注意一下名字上的規(guī)范,在中文里,“結(jié)構(gòu)”和“類(lèi)”可以放在“記錄”這個(gè)詞語(yǔ)的前面或者后面都行,但在英語(yǔ)環(huán)境下,我們是固定放在 record 這個(gè)單詞的后面的,即 record class 和 record struct,而不是 struct record 或 class record,請(qǐng)一定要注意。

結(jié)構(gòu)記錄類(lèi)型的底層

和記錄類(lèi)一樣,記錄結(jié)構(gòu)也是生成了完全一致的這些成員:

  • 一個(gè)帶有這些屬性對(duì)應(yīng)賦值的構(gòu)造器;

  • Equals 的重寫(xiě)方法(簽名大概是 bool Equals(object));

  • Equals 方法,參數(shù)不是 object 而是這個(gè)數(shù)據(jù)類(lèi)型本身(簽名大概是 bool Equals(T));

  • GetHashCode 重寫(xiě)方法(簽名大概是 int GetHashCode());

  • ToString 重寫(xiě)方法(簽名大概是 string ToString());

  • Deconstruct 解構(gòu)方法(簽名大概是 void Deconstruct(...),參數(shù)都是 out 類(lèi)型的,把每一個(gè)寫(xiě)在小括號(hào)的數(shù)據(jù)成員全部挨個(gè)寫(xiě)入到這里當(dāng)參數(shù));

  • 運(yùn)算符 ==!=,參數(shù)是這個(gè)類(lèi)型自己(簽名分別是 operator ==(T, T)operator !=(T, T));

  • 一個(gè) private 或者 protected 修飾的 PrintMembers 方法(簽名大概是 bool PrintMembers(StringBuilder))。

不過(guò)少了一個(gè) Clone 方法和一個(gè)復(fù)制構(gòu)造器。這個(gè)原因也很簡(jiǎn)單:因?yàn)榻Y(jié)構(gòu)的等號(hào)就自帶拷貝副本的光環(huán),你在使用的時(shí)候,因?yàn)橐妙?lèi)型的等號(hào)賦值過(guò)程都是賦值地址數(shù)據(jù)(引用),所以為了支持?jǐn)?shù)據(jù)拷貝,所以有 Clone 才可以搞定;但在結(jié)構(gòu)里,等號(hào)就有一樣的效果了,因此就沒(méi)有了這兩個(gè)專(zhuān)門(mén)用來(lái)拷貝用的成員了。

然后,少了一個(gè) EqualityContract,因?yàn)闆](méi)有必要有了。

而且,這些剩余的成員,生成的底層代碼也都和記錄類(lèi)里生成的內(nèi)容是完全一樣的,所以沒(méi)有必要刻意說(shuō)明細(xì)節(jié);不過(guò)還是有一個(gè)要說(shuō)的地方,就是這個(gè) readonly 修飾符的問(wèn)題。

readonly record structrecord struct 底層代碼的區(qū)別

區(qū)別一:部分自動(dòng)生成的成員是否標(biāo)記 readonly 修飾符

C# 7 帶來(lái)了 readonly struct 的概念,而 C# 8 則帶來(lái)了 readonly 結(jié)構(gòu)成員的概念,因此 readonly 關(guān)鍵字就得在這里好好說(shuō)說(shuō)了。

先回顧一下 readonly 修飾符的基本用法:

  • 修飾到字段上,表示字段的數(shù)據(jù)在構(gòu)造器和自身賦值語(yǔ)句初始化后不再更改;

  • 修飾到結(jié)構(gòu)上,表示結(jié)構(gòu)里的所有實(shí)例成員(構(gòu)造器除外)都是只讀的;

  • 修飾到結(jié)構(gòu)里的實(shí)例成員(構(gòu)造器和字段除外)上,表示結(jié)構(gòu)里的所有這些成員在執(zhí)行期間都不會(huì)發(fā)生該類(lèi)型數(shù)據(jù)成員的數(shù)據(jù)在底層的變動(dòng)。

這里我們只用得上后面這兩種情況。比如說(shuō)我現(xiàn)在有一個(gè)結(jié)構(gòu) Student,里面有一個(gè)屬性 AverageScore,它只有 get 方法,目的是獲取這個(gè)學(xué)生實(shí)例的學(xué)習(xí)平均分。按照規(guī)范執(zhí)行,我們必須得這么實(shí)現(xiàn)代碼:

或者你直接改寫(xiě)用 Lambda 寫(xiě)法簡(jiǎn)記:

可以看出,我們完完全全只是在讀取里面的數(shù)據(jù),而怎么也不會(huì)通過(guò)這個(gè) get 方法去改變比如 Math、EnglishChinese 的數(shù)值。因此,我們稱(chēng)這樣的屬性是只讀的。按照 C# 8 提供的語(yǔ)法,我們需要使用 readonly 修飾符修飾到屬性上:

這樣可以更加嚴(yán)謹(jǐn)一些,通過(guò)語(yǔ)法層面來(lái)限定,以便以后優(yōu)化代碼。

回到這里。說(shuō)這個(gè)是干什么呢?readonly record structrecord struct 的區(qū)別在于,底層生成的代碼都帶不帶 readonly 修飾符的這個(gè)問(wèn)題。

如果是 readonly record struct,因?yàn)榇蠹叶贾肋@個(gè)類(lèi)型都標(biāo)記了 readonly 了,那么自然里面所有成員都得遵章守紀(jì)按照不修改數(shù)據(jù)的方式來(lái)獲取數(shù)值,因此所有實(shí)例成員都不必再標(biāo)記 readonly 了,因?yàn)楸緛?lái)就有了 readonly 了還標(biāo)記重復(fù)的 readonly 就沒(méi)有意義。當(dāng)然,這只針對(duì)于非字段的實(shí)例成員。字段最開(kāi)始就必須要求 readonly 修飾符,它的 readonly 不可省略。

但是,如果這個(gè)記錄結(jié)構(gòu)本身沒(méi)有 readonly 修飾符的話(huà),這意味著里面的某個(gè)或某些成員可能會(huì)在實(shí)例化后仍然可變(數(shù)值發(fā)生變化)。這種情況下,我們只能對(duì)一部分不更改變動(dòng)底層數(shù)值的實(shí)例成員標(biāo)記 readonly 了。此時(shí),我們回顧一下剛才我們說(shuō)到的自動(dòng)生成的成員,可以發(fā)現(xiàn),里面部分的成員(比如 ToString 方法、Equals 方法)都只是取值來(lái)得到輸出結(jié)果的目的,它們不會(huì)變更數(shù)值。因此,在底層代碼里,這些成員會(huì)在自動(dòng)生成的代碼上帶上 readonly 修飾符。

區(qū)別二:initset 屬性賦值器

是的。C# 10 的結(jié)構(gòu)記錄在這點(diǎn)上是有區(qū)分的。因?yàn)?readonly 修飾符的特性告知了用戶(hù)這個(gè)類(lèi)型是否可變,因此我們強(qiáng)制用戶(hù)必須優(yōu)先考慮加上 readonly 修飾符到 record struct 的聲明上去;如果確實(shí)可變,那么我們則可以考慮用戶(hù)不加 readonly 修飾符。

那么可變和不可變體現(xiàn)的地方就在這個(gè)屬性生成的代碼上。

我們對(duì)比兩個(gè)聲明,主構(gòu)造器的參數(shù)表列全部是一樣的,只是聲明的時(shí)候一個(gè)有 readonly 一個(gè)沒(méi)有。在底層,這三個(gè)屬性在底層里是生成了帶有 getinit 的自動(dòng)屬性,而在沒(méi)有 readonly 修飾的記錄結(jié)構(gòu)里,這三個(gè)屬性在底層里則是生成了帶有 getset 的自動(dòng)屬性。即大概是這樣的:

這是它們的第二個(gè)區(qū)別。

你可能會(huì)問(wèn)我。既然有賦值器,那么說(shuō)明這個(gè)屬性是可變的啊,那么為什么 readonly record struct 又可以修飾 readonly 呢?這不是矛盾了嗎?實(shí)際上并不是。這個(gè) init 修飾符保證了賦值過(guò)程只發(fā)生在初始化器里,也就是說(shuō),它只能用在 new 表達(dá)式的后面接著大括號(hào),里面包含的這個(gè)初始化器的內(nèi)容一起構(gòu)造成為整體。而 init 保證了賦值過(guò)程只出現(xiàn)在這里,所以屬性只是在初始化的時(shí)候發(fā)生了變更,在使用的時(shí)候完全沒(méi)有,這不還是跟構(gòu)造器實(shí)例化對(duì)象是一個(gè)效果嗎?所以說(shuō),在結(jié)構(gòu)里,一個(gè) readonly struct 是允許屬性包含 init 賦值器的,而 set 賦值器卻不行。

區(qū)別三:主構(gòu)造器參數(shù)和非合成屬性重復(fù)報(bào)錯(cuò)

在 C# 9 的記錄類(lèi)里,如果出現(xiàn)了主構(gòu)造器參數(shù)和屬性重名的情況,會(huì)產(chǎn)生編譯器警告;但是在 C# 10 里的這個(gè)記錄結(jié)構(gòu)里,就不是編譯器警告了,而是編譯器錯(cuò)誤。

比如這樣的代碼,屬性 X 和主構(gòu)造器參數(shù) X 重名了。此時(shí)因?yàn)?Pos 是記錄結(jié)構(gòu),因此將產(chǎn)生編譯器錯(cuò)誤,而不是編譯器警告。那么為什么不統(tǒng)一呢?因?yàn)樵缙谶@個(gè)錯(cuò)誤信息只是一個(gè)較弱的約束,而產(chǎn)生了記錄結(jié)構(gòu)后,相當(dāng)于是升級(jí)版的記錄類(lèi)型,但又為了保持語(yǔ)法的兼容,原本的級(jí)別沒(méi)有得到調(diào)整,但現(xiàn)在大家都知道了,X 重名后就必然導(dǎo)致 X 有一個(gè)完全無(wú)法用到,所以編譯器錯(cuò)誤才應(yīng)該是更合適的報(bào)錯(cuò)級(jí)別。因此報(bào)錯(cuò)的級(jí)別并不一致,是這個(gè)原因。

記錄結(jié)構(gòu)的 with 表達(dá)式

在標(biāo)題上,我直接把“記錄結(jié)構(gòu)”的“記錄”給劃掉了??赡苣愫茉尞悾疫@行為是在干嘛呢?還記得 with 表達(dá)式在 C# 9 的記錄類(lèi)里是怎么用的嗎?

即初始化了實(shí)例后,仍可以通過(guò) with 關(guān)鍵字“小幅度調(diào)整”對(duì)象里的數(shù)據(jù)成員信息,然后把 a with { ... } 整個(gè)表達(dá)式用這個(gè)得到的修改后的對(duì)象給直接替換掉。而在這個(gè)期間,底層是會(huì)調(diào)用 Clone 方法來(lái)復(fù)制副本的,這樣才能保證這里的 ab 完全是兩個(gè)不同引用的對(duì)象。

但是,這一點(diǎn)在 C# 10 的記錄結(jié)構(gòu)里就顯得沒(méi)有必要了。結(jié)構(gòu)是不需要 Clone 的,也不需要復(fù)制構(gòu)造器的,所以 C# 10 干脆開(kāi)放了語(yǔ)法,讓所有結(jié)構(gòu)(不管是不是 record struct)全部都可以直接用 with 表達(dá)式了。

當(dāng)然了,如果是 record struct 肯定是可以用 with 表達(dá)式的:

用法則和之前的 with 沒(méi)啥區(qū)別。

結(jié)構(gòu)初始化行為相關(guān)語(yǔ)法的調(diào)整

為了讓記錄結(jié)構(gòu)更加靈活,C# 語(yǔ)言團(tuán)隊(duì)不得不調(diào)整對(duì)結(jié)構(gòu)的一些初始化行為的邏輯。

請(qǐng)注意,下面的內(nèi)容會(huì)直接顛覆和改變你對(duì)原本 C# 里結(jié)構(gòu)的用法邏輯和理解方式。請(qǐng)一定要注意 C# 10 改變了結(jié)構(gòu)的初始化邏輯和規(guī)則。

要知道,C# 的結(jié)構(gòu)是非常輕量級(jí)的數(shù)據(jù)類(lèi)型,它的出現(xiàn)引出了很多初始化的基本概念。比如說(shuō),它的語(yǔ)法是合取了所有內(nèi)置的那些數(shù)據(jù)類(lèi)型的初始化方式,才得到了這些結(jié)論:

  • 這些數(shù)據(jù)類(lèi)型初始化之前必須得有數(shù)值傳入;

  • 這些數(shù)據(jù)因?yàn)榛緮?shù)據(jù)類(lèi)型,所以必須預(yù)先準(zhǔn)備好固定的分配內(nèi)存規(guī)則,畢竟值類(lèi)型一般會(huì)被放進(jìn)棧內(nèi)存。

出于這些基本限制,C# 早期做出了這些限制:

  • 必須帶有一個(gè)用戶(hù)無(wú)法更改的無(wú)參構(gòu)造器,用于默認(rèn)初始化內(nèi)存用;

  • 所有數(shù)據(jù)成員均無(wú)法手動(dòng)初始化(即在成員的默認(rèn)補(bǔ)充 = 數(shù)值; 的賦值部分)。

下面我們針對(duì)于這樣兩個(gè)內(nèi)容來(lái)描述一下,變更的規(guī)則是如何的。

無(wú)參構(gòu)造器

早期來(lái)說(shuō),C# 的無(wú)參構(gòu)造器是客觀(guān)存在的,不論你是否聲明了別的構(gòu)造器,無(wú)參構(gòu)造器都是客觀(guān)存在的;而且,正是因?yàn)檫@個(gè)原因,C# 甚至不讓你自定義無(wú)參構(gòu)造器。雖然無(wú)參構(gòu)造器長(zhǎng)這樣:

即使寫(xiě)法上很簡(jiǎn)單,但 C# 仍然不讓你自己寫(xiě)。雖然它一般都和內(nèi)存分配綁定起來(lái),可問(wèn)題就在于,這么做一個(gè)限定讓我們初始化一些數(shù)據(jù)的時(shí)候極為不便,因?yàn)橛行r(shí)候我就希望一些數(shù)據(jù)在初始化的時(shí)候就有不同的數(shù)值,而如果我們嘗試調(diào)用有參構(gòu)造器的話(huà),又會(huì)產(chǎn)生冗余參數(shù),而我只是想自定義一個(gè)默認(rèn)的初始化行為而已,這樣 C# 早期的語(yǔ)法就無(wú)法做到。

為了避免這樣的不便,C# 10 作出了妥協(xié):允許用戶(hù)可定義一個(gè)必須是 public 的無(wú)參構(gòu)造器,這樣的話(huà),用戶(hù)就可以自定義初始化行為了。不過(guò)要注意的是,必須是 public 修飾,別的訪(fǎng)問(wèn)修飾符都不行。因?yàn)槟慵热欢荚敢飧某跏蓟男袨榱耍珮?gòu)造器一直都必須調(diào)用,以得到正常的初始化效果,那么無(wú)參構(gòu)造器設(shè)置為不公開(kāi)的情況的話(huà),那么該有的限制還是沒(méi)有。因此,無(wú)參構(gòu)造器仍然必須是 public 的。

當(dāng)然,還有一個(gè)只能定義為 public 的原因,也是最為主要的原因:因?yàn)榉奖憔幾g器分析。如果一旦出現(xiàn)非 public 的構(gòu)造器的話(huà),一個(gè)結(jié)構(gòu)的初始化行為的復(fù)雜度就會(huì)高出不止一個(gè)級(jí)別。比如我只有一個(gè) private 的無(wú)參構(gòu)造器是自定義的,那么就會(huì)影響到我后期比如使用反射創(chuàng)建實(shí)例化對(duì)象,以及 new() 泛型約束檢測(cè)它是不是包含無(wú)參構(gòu)造器之類(lèi)的。所以,C# 10 干脆就直接限制你不讓你創(chuàng)建非 public 的自定義無(wú)參構(gòu)造器。

不過(guò),由于無(wú)參構(gòu)造器可以用戶(hù)自行定義后,就會(huì)影響一系列的語(yǔ)法規(guī)則。

第一,default 表達(dá)式。其實(shí)也很好理解,因?yàn)闊o(wú)參構(gòu)造器有了之后,初始化默認(rèn)行為就改變了,以至于結(jié)構(gòu)里,default(T)new T() 的語(yǔ)義不再一樣。你始終記住,default 表達(dá)式永遠(yuǎn)都是那個(gè)早期的那種、所有數(shù)據(jù)成員都是這個(gè)數(shù)據(jù)類(lèi)型自身的默認(rèn)數(shù)值,所構(gòu)建出來(lái)的實(shí)例結(jié)果;而現(xiàn)如今的 new T() 則是兩種情況:

  • 如果有自定義無(wú)參構(gòu)造器,那么 new T() 按現(xiàn)在定義的那樣計(jì)算初始化結(jié)果;

  • 如果沒(méi)有自定義無(wú)參構(gòu)造器,那么 new T() 就和 default(T) 是一樣的。

第二,new() 泛型約束。C# 2 的泛型引入了 where T : new() 的這種約束模式,它約束這個(gè)對(duì)象必然有無(wú)參構(gòu)造器。如果一個(gè)泛型類(lèi)型的約束是這樣的的話(huà):

那么它會(huì)不會(huì)受到影響呢?不會(huì)。因?yàn)闊o(wú)參構(gòu)造器不管你自己定義還是系統(tǒng)自己生成,這個(gè)都不會(huì)影響,因?yàn)閮煞N情況下,無(wú)參構(gòu)造器都是有的。所以,大大方方用吧,這一點(diǎn)來(lái)說(shuō)是沒(méi)有差別的。

第三,迭代結(jié)構(gòu)類(lèi)型。雖然 new() 泛型約束不受影響,但是對(duì)迭代類(lèi)型來(lái)說(shuō)的話(huà),就不太一樣了。所謂的迭代類(lèi)型,比如下面的例子就是一個(gè)良好的描述:

這個(gè) S1 結(jié)構(gòu)類(lèi)型里有一個(gè) S0 結(jié)構(gòu)類(lèi)型的字段,而 S 結(jié)構(gòu)類(lèi)型是一個(gè)泛型類(lèi)型,它包含一個(gè)泛型參數(shù) T 類(lèi)型的字段,而這個(gè) T 則是一個(gè)結(jié)構(gòu)。

這里我們要說(shuō)一下有點(diǎn)奇怪的規(guī)則。為了達(dá)到 C# 10 的無(wú)參構(gòu)造器定義和不影響初始化行為的規(guī)則,此時(shí)這兩個(gè) F 字段(分別位于 ?S1 類(lèi)型和 S<T> 類(lèi)型里)是如何初始化的呢?答案是,忽略掉。是的,直接忽略掉。無(wú)參構(gòu)造器只提供了一種你自定義初始化行為的手段,但它仍然不影響任何時(shí)候其它地方系統(tǒng)的初始化過(guò)程。

比如 S1 類(lèi)型的 F 字段,按照初始化的行為規(guī)則,F 因?yàn)槭亲侄危员仨氃诔跏蓟瘜?shí)例之前給結(jié)構(gòu)的所有成員賦值。而如果 S1 沒(méi)有任何自定義的構(gòu)造器的話(huà),那么這個(gè)字段將保持默認(rèn)數(shù)值。默認(rèn)數(shù)值是多少,我剛才說(shuō)過(guò)了吧??蓡?wèn)題就在于,此時(shí) S0 類(lèi)型有一個(gè)我們定義了的無(wú)參構(gòu)造器,所以這個(gè) F 初始化的時(shí)候,我們?nèi)匀皇遣豢催@個(gè)構(gòu)造器的;取而代之的是,default(S0)作為 F 字段的默認(rèn)值。是的,更嚴(yán)謹(jǐn)?shù)恼Z(yǔ)言是,所有字段默認(rèn)初始化為 default(T) 結(jié)果,而并不是 new T() 結(jié)果。這個(gè)需要你記清楚。

而第二個(gè)例子里,S 類(lèi)型是一個(gè)泛型類(lèi)型,包含一個(gè)結(jié)構(gòu)類(lèi)型的泛型參數(shù)。那么如果把這個(gè)泛型類(lèi)型的參數(shù)作為類(lèi)型,創(chuàng)建一個(gè)字段放在這個(gè)類(lèi)型里的話(huà),這個(gè) F 參照的是什么初始化表達(dá)式呢?答對(duì)了,還是 default(T),仍然不是 new T()。因此,一定要注意這里。

第四,base() 基類(lèi)型構(gòu)造器的調(diào)用。雖然結(jié)構(gòu)沒(méi)有什么所謂的基類(lèi)型的概念,因?yàn)樗约菏菬o(wú)法自定義繼承和派生關(guān)系的。但是,別忘了它隱式從 ValueType 這個(gè)類(lèi)派生下來(lái)的,而 ValueType 是一個(gè)抽象類(lèi),不允許你直接實(shí)例化。問(wèn)題來(lái)了,我如果有一個(gè)結(jié)構(gòu) S,那么我既然能夠定義無(wú)參構(gòu)造器了,那么是不是意味著我能夠調(diào)用 base() 來(lái)獲取 ValueType 里的無(wú)參構(gòu)造器的初始化過(guò)程呢?

答案是,不可以。C# 10 仍禁止你調(diào)用 base()。倒不是因?yàn)?ValueType 是抽象類(lèi)所以沒(méi)有無(wú)參構(gòu)造器,而是因?yàn)椴蛔屇阌谩?/span>

第五,結(jié)構(gòu)類(lèi)型元素的數(shù)組的初始化。還是這個(gè)熟悉的配方。我們?nèi)匀皇褂?S 來(lái)表示一個(gè)結(jié)構(gòu)類(lèi)型,并且假設(shè)定義好了一個(gè)無(wú)參構(gòu)造器。那么如果我這么定義:

那么是不是 s 變量的每一個(gè)元素都調(diào)用了 new T() 這個(gè)自定義的無(wú)參構(gòu)造器呢?答案是,否定。它們的初始化仍然用的是 default(T) 的結(jié)果而不是 new T() 的結(jié)果。

第六,可選參數(shù)和變長(zhǎng)參數(shù)導(dǎo)致的假無(wú)參構(gòu)造。

如果我做了上述的代碼書(shū)寫(xiě)的話(huà),那么請(qǐng)問(wèn)兩次實(shí)例化有哪個(gè)(哪些)是會(huì)輸出 42 的呢?

答案是,一個(gè)都沒(méi)有。無(wú)參構(gòu)造器是最匹配的項(xiàng),而有可選參數(shù)和變長(zhǎng)參數(shù)的構(gòu)造器只是在書(shū)寫(xiě)的時(shí)候可以有假的無(wú)參的寫(xiě)法表現(xiàn),但并不意味著真的無(wú)參。所以按照匹配的優(yōu)先級(jí)來(lái)說(shuō),它們是比無(wú)參構(gòu)造器低一級(jí)的,因此,兩個(gè)都是調(diào)用的無(wú)參構(gòu)造器,而上面的代碼沒(méi)有寫(xiě),因此啥都不輸出,因此你看不到輸出 42 的情況。

最后一點(diǎn),就是反射里調(diào)用無(wú)參構(gòu)造器輸出的情況。這個(gè)和泛型約束 new() 的結(jié)論差不多。因?yàn)榻Y(jié)構(gòu)此時(shí)必須是 public 的,所以一定有可訪(fǎng)問(wèn)的無(wú)參構(gòu)造器,因此不論你自定義與否,它都是客觀(guān)存在的,因此并不會(huì)影響。假設(shè)你要使用比如 Activator.CreateInstance<T>() 的語(yǔ)法創(chuàng)建一個(gè)結(jié)構(gòu)類(lèi)型實(shí)例的話(huà),這么做也是永遠(yuǎn)都成功的。

成員初始化器

無(wú)參構(gòu)造器我們說(shuō)完了,下面我們來(lái)說(shuō)一下成員初始化器(Member Initializer)。成員初始化器這個(gè)名字跟初始化器差不多,但用法上不同。一般我們說(shuō)初始化器都指的是對(duì)象初始化器(Object Initializer),書(shū)寫(xiě)格式是大括號(hào),里面跟上屬性等于數(shù)值的鍵值對(duì)的賦值過(guò)程;而這里說(shuō)的成員初始化器,則指的是直接在成員的聲明語(yǔ)句的末尾追加 = 數(shù)值; 的初始化部分。那么與其說(shuō)是成員初始化器,還不如嚴(yán)謹(jǐn)一點(diǎn)叫它數(shù)據(jù)成員初始化器,因?yàn)檫@種初始化過(guò)程只發(fā)生在字段和自動(dòng)屬性上。

從 C# 10 開(kāi)始,結(jié)構(gòu)和類(lèi)的初始化過(guò)程就越來(lái)越類(lèi)似了。當(dāng)然,為了保留和兼容之前 C# 的定義規(guī)則,初始化器仍然也不是那么像類(lèi)的初始化過(guò)程。

首先我們來(lái)回顧一下類(lèi)的初始化過(guò)程:類(lèi)類(lèi)型的實(shí)例會(huì)調(diào)用 new 后給的構(gòu)造器,傳入?yún)?shù),并給數(shù)據(jù)賦值。如果沒(méi)有賦值到的對(duì)象,則剩余的數(shù)據(jù)成員將會(huì)挨個(gè)按照代碼順序從上往下挨個(gè)賦值,賦的值是 default(T),即它自己這個(gè)類(lèi)型的默認(rèn)數(shù)值。

而結(jié)構(gòu)的話(huà),初始化也差不多了。只不過(guò),因?yàn)榻Y(jié)構(gòu)在初始化器里必須對(duì)所有對(duì)象要完成初始化,因此這個(gè)限制在 C# 10 里只推廣了一丟丟:結(jié)構(gòu)里的數(shù)據(jù)成員必須經(jīng)過(guò)成員初始化器或構(gòu)造器完成初始化。如果構(gòu)造器里不初始化的話(huà),那么這個(gè)數(shù)據(jù)成員必須包含成員初始化器;否則編譯器將產(chǎn)生錯(cuò)誤。這和類(lèi)類(lèi)型不同:類(lèi)類(lèi)型是不賦值也可以,它會(huì)自動(dòng)得到默認(rèn)數(shù)值;而結(jié)構(gòu)類(lèi)型則必須添加成員初始化器以避免編譯器報(bào)錯(cuò),哪怕你知道這里賦值只是一個(gè)簡(jiǎn)單的 default(T) 你也得寫(xiě)上;除非,它可以在對(duì)象初始化器里賦值,即這個(gè)屬性是 public 的非 readonly 實(shí)例字段,或者是帶有 init 的屬性。這樣的話(huà)就不用要求你必須在構(gòu)造器里賦值了,因?yàn)樗梢詣e的地方賦值。

這個(gè) Prop3 是我胡謅上去的寫(xiě)法,就是為了闡述和表達(dá)出它是帶有 init 賦值器的屬性。這個(gè)屬性因?yàn)榭梢栽趯?duì)象初始化器里賦值,因此可以不在構(gòu)造器里賦值的同時(shí),補(bǔ)上成員初始化器。

成員的合成和非合成

合成成員

和 C# 9 的記錄類(lèi)是差不多的,概念我就不提了,概念是一樣的。

自定義成員

這一點(diǎn)和 C# 9 的記錄類(lèi)也是差不多的。比如我在 Student 記錄類(lèi)型里加入可變的屬性成員 Class

此時(shí)語(yǔ)法上是沒(méi)有問(wèn)題的,編譯器也不會(huì)認(rèn)為你這么寫(xiě)代碼會(huì)有問(wèn)題。不過(guò),因?yàn)?Person 記錄結(jié)構(gòu)上沒(méi)有 readonly 修飾符,因此這個(gè) Classgetset 的寫(xiě)法,是和 NameAgeGender 底層生成的代碼是一樣的,都是 getset 的自動(dòng)屬性。而且,它也要參與數(shù)據(jù)成員的校驗(yàn)和使用:

這個(gè)表格和 C# 9 的記錄類(lèi)是一樣的。

記錄結(jié)構(gòu)類(lèi)型的繼承和派生機(jī)制

因?yàn)榻Y(jié)構(gòu)必須從 ValueType 派生,所以由于結(jié)構(gòu)無(wú)法自定義派生規(guī)則的關(guān)系,我們無(wú)法對(duì)記錄結(jié)構(gòu)定義派生和繼承關(guān)系,不過(guò)你可以要求它實(shí)現(xiàn)一些接口,這個(gè)和 C# 9 的記錄類(lèi)是一樣的。

接口的話(huà),和 C# 9 的記錄類(lèi)是一樣的。主構(gòu)造器的參數(shù)直接視為一個(gè)個(gè)的屬性即可。只是要注意,readonly record structgetinit 的自動(dòng)屬性,但 record structgetset 的屬性。如果關(guān)鍵字用得不一樣,接口就會(huì)和這里給的屬性本身不匹配,導(dǎo)致編譯器錯(cuò)誤,提示你沒(méi)有實(shí)現(xiàn)完成員。

和 C# 9 的記錄類(lèi)一樣的地方,除了剛才說(shuō)的地方,還有一個(gè)點(diǎn),就是實(shí)現(xiàn)接口。記錄結(jié)構(gòu)類(lèi)型在寫(xiě)上主構(gòu)造器后,因?yàn)樗鼤?huì)自動(dòng)生成 Equals 方法,所以這個(gè)記錄結(jié)構(gòu)類(lèi)型也會(huì)自動(dòng)幫你實(shí)現(xiàn) IEquatable<T> 接口。所以你可以直接把這個(gè)類(lèi)型拿去參與相等性比較,比如使用 Equals 方法,或者是 ==!= 運(yùn)算符。

其它無(wú)關(guān)痛癢的記錄類(lèi)型語(yǔ)法

partial 修飾符修飾記錄類(lèi)型

如果我們要用 partial 修飾符來(lái)修飾記錄類(lèi)型,是怎么樣用的呢?和記錄類(lèi)是一樣的寫(xiě)法,只是原本的 partial record 的配方要改成 partial record struct 了。

比如這樣。不過(guò)一定請(qǐng)注意,partial 必須放在 record struct 的前面。也就是說(shuō),record struct 這個(gè)時(shí)候是一個(gè)標(biāo)識(shí)整體,我們無(wú)法把 partial 關(guān)鍵字插入到 recordstruct 關(guān)鍵字的中間,它們是不能拆開(kāi)的。

record class 的語(yǔ)義

因?yàn)榕浜?C# 10 的 record struct 的定義規(guī)則,C# 10 推廣了 record 的零碎語(yǔ)法。record 從語(yǔ)義上和 record class 這個(gè)定義組合是等價(jià)的,所以在 C# 10 里,編譯器允許我們?cè)?record 關(guān)鍵字后再加上 class 關(guān)鍵字表示一個(gè)記錄類(lèi)類(lèi)型。這個(gè)寫(xiě)法和原本的 record 沒(méi)有區(qū)別,它的出現(xiàn)用于強(qiáng)調(diào)和區(qū)分現(xiàn)有的 record struct 的寫(xiě)法。

record struct 的基本用法一樣,record class 也是不可拆分的單位,因此你也只能使用比如 partial record class 這樣的語(yǔ)法。

主構(gòu)造器允許的參數(shù)修飾符

好吧,這一點(diǎn)和 C# 9 記錄類(lèi)是一樣的,仍然只允許我們使用 inparams 修飾符修飾參數(shù)。

無(wú)參主構(gòu)造器

記錄結(jié)構(gòu)的無(wú)參主構(gòu)造器可否存在呢?可以。比如長(zhǎng)這樣:

是的,這一對(duì)小括號(hào)里沒(méi)有寫(xiě)東西,所以它也可以不寫(xiě)出來(lái):

這樣要好看一點(diǎn)。

主構(gòu)造器上使用特性

這一點(diǎn)也和 C# 9 記錄類(lèi)的是一樣的。

沒(méi)有 record interface

雖然我們知道,structclass 都可以使用 record 來(lái)簡(jiǎn)化語(yǔ)義模型構(gòu)造 POCO 了,但 interface 是純抽象的對(duì)象類(lèi)別,所以沒(méi)有 record interface 一說(shuō),畢竟……接口自身肯定不能實(shí)例化嘛。

沒(méi)有 record ref struct

是的,雖然 ref struct 很好用,有時(shí)候我們也可以把一個(gè)很簡(jiǎn)單的 ref struct 給調(diào)整成一個(gè) POCO,但它畢竟不是一個(gè)合規(guī)的結(jié)構(gòu),因?yàn)樗荒芊旁跅?nèi)存里,很多特殊的規(guī)則就不適用了,比如值類(lèi)型的裝箱,比如泛型參數(shù)之類(lèi)。ref struct 的條件甚至有點(diǎn)過(guò)于嚴(yán)苛了,因此 ref struct 這種組合是沒(méi)有記錄對(duì)應(yīng)寫(xiě)法的,也就是說(shuō),你無(wú)法寫(xiě)成比如 record ref struct 這樣的東西,因?yàn)?C# 10 的記錄結(jié)構(gòu)并不支持針對(duì)于 ref struct 的情況。


探索 C# 10 的記錄結(jié)構(gòu)類(lèi)型的評(píng)論 (共 條)

分享到微博請(qǐng)遵守國(guó)家法律
古丈县| 凉城县| 叶城县| 尖扎县| 柳河县| 大同市| 印江| 柯坪县| 山丹县| 波密县| 达日县| 乳山市| 锡林浩特市| 吴江市| 政和县| 郧西县| 安吉县| 福建省| 巨野县| 柏乡县| 镇康县| 广德县| 永春县| 涡阳县| 莱西市| 铜鼓县| 繁昌县| 辉南县| 都昌县| 北安市| 洪洞县| 绿春县| 闵行区| 平遥县| 东莞市| 白山市| 石楼县| 金溪县| 沁源县| 石台县| 苏州市|