探索 C# 10 的記錄結(jié)構(gòu)類(lèi)型
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 struct
和 record 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)記:
get
方法去改變比如 Math
、English
和 Chinese
的數(shù)值。因此,我們稱(chēng)這樣的屬性是只讀的。按照 C# 8 提供的語(yǔ)法,我們需要使用 readonly
這樣可以更加嚴(yán)謹(jǐn)一些,通過(guò)語(yǔ)法層面來(lái)限定,以便以后優(yōu)化代碼。
回到這里。說(shuō)這個(gè)是干什么呢?readonly record struct
和 record 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ū)別二:init
和 set
屬性賦值器
是的。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è)屬性在底層里是生成了帶有 get
和 init
的自動(dòng)屬性,而在沒(méi)有 readonly
修飾的記錄結(jié)構(gòu)里,這三個(gè)屬性在底層里則是生成了帶有 get
和 set
的自動(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ò)
比如這樣的代碼,屬性 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ù)制副本的,這樣才能保證這里的 a
和 b
完全是兩個(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
:
Person
記錄結(jié)構(gòu)上沒(méi)有 readonly
修飾符,因此這個(gè) Class
的 get
和 set
的寫(xiě)法,是和 Name
、Age
和 Gender
底層生成的代碼是一樣的,都是 get
和 set

記錄結(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 struct
是 get
和 init
的自動(dòng)屬性,但 record struct
是 get
和 set
的屬性。如果關(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)鍵字插入到 record
和 struct
關(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)是一樣的,仍然只允許我們使用 in
和 params
修飾符修飾參數(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
雖然我們知道,struct
和 class
都可以使用 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
的情況。