第 42 講:結(jié)構(gòu)(一):值類型的定義和結(jié)構(gòu)的自定義
各位已經(jīng)接觸到了一些值類型和引用類型的基本概念。為了銜接本版塊的內(nèi)容,我們還要重新介紹一次。不過因?yàn)橐佑|具體內(nèi)容了,因此我們要說得更細(xì)一點(diǎn)。
Part 1 值類型和引用類型
C# 把所有的數(shù)據(jù)類型全部按照值類型(Value Type)和引用類型(Reference Type)分為兩類。值類型可根據(jù)數(shù)據(jù)類型的安放和存儲(chǔ)對(duì)應(yīng)進(jìn)入堆內(nèi)存或棧內(nèi)存里;而引用類型只能存儲(chǔ)在堆內(nèi)存里。
之前有簡單說過,堆內(nèi)存相對(duì)于運(yùn)算過程來說要遠(yuǎn)一些,所以存儲(chǔ)和取出數(shù)值可能會(huì)稍微慢一點(diǎn);而棧內(nèi)存更快。因此,如果我們需要優(yōu)化計(jì)算速度的話,應(yīng)該優(yōu)先考慮值類型(因?yàn)橹殿愋涂梢源鎯?chǔ)在棧內(nèi)存里,而引用類型不能)。
另外,值類型和引用類型的存儲(chǔ)規(guī)則是無法修改的,你只能通過代碼的方式來使用它們;如果你需要改變存儲(chǔ)規(guī)則,唯一的辦法就是把變量的類型從值類型改成引用類型;或者從引用類型改成值類型。
另一方面,值類型是可存儲(chǔ)在棧內(nèi)存里的,因此存儲(chǔ)在棧內(nèi)存里的這一部分值類型數(shù)據(jù)是不受到垃圾回收器 GC 的管理的。GC 在之前已經(jīng)說過了它的基本執(zhí)行模式:找到不再使用的堆內(nèi)存數(shù)據(jù)空間,然后銷毀掉后,通過緊湊處理把數(shù)據(jù)壓縮放在一起避免零碎的空間。但是,因?yàn)樗还茌牰褍?nèi)存,而棧內(nèi)存是通過方法自身的調(diào)用和釋放而自動(dòng)產(chǎn)生內(nèi)存空間和銷毀空間的,因此和 GC 無關(guān)。要知道,GC 怎么著都會(huì)處理比棧內(nèi)存存儲(chǔ)更多的數(shù)據(jù),那么速度顯然就會(huì)比棧內(nèi)存慢,因此值類型更好的一點(diǎn)是優(yōu)化存儲(chǔ)機(jī)制。
所以,我們這里知道了兩個(gè)值類型比引用類型更好的地方:
值類型存儲(chǔ)在棧內(nèi)存的這一塊,自動(dòng)受到方法本身管理,不受 GC 管理;
值類型計(jì)算速度更快。
之前我們講的接口、類都是引用類型,因?yàn)樗鼈兺驾^大,所以放在堆內(nèi)存里是正合適的一種手段。但是,我們需要用到棧內(nèi)存的時(shí)候,卻因?yàn)檎Z法不能支持,導(dǎo)致很頭疼的境況。下面我們來說一下,一個(gè)值類型應(yīng)該如何自定義。
Part 2 結(jié)構(gòu)的定義
下面,我們引入一種和類、接口的地位同等重要的另外一種自定義的數(shù)據(jù)類型:結(jié)構(gòu)(Structure)。結(jié)構(gòu)和類的定義方式基本完全一致,唯一的區(qū)別是,把類的 class
關(guān)鍵字改寫成 struct
。
我們唯一改變的地方就是 class
改成了 struct
,別的地方一點(diǎn)變化都沒有。這個(gè) Person
此時(shí)被稱為一個(gè)結(jié)構(gòu)(即 Person
結(jié)構(gòu))。它和一般的類基本用法都差不多,但是有一些細(xì)節(jié)可能和類不一樣。下面我們簡單說一下。
Part 3 結(jié)構(gòu)在使用上和類不一樣的地方
剛才說到,結(jié)構(gòu)和類的定義差別僅僅是在 struct
改成了 class
,那么它們細(xì)節(jié)上又有什么不同的地方呢?
在類里,如果不定義無參構(gòu)造器的話,系統(tǒng)會(huì)自動(dòng)生成一個(gè),而且里面啥操作都沒有的無參構(gòu)造器。在結(jié)構(gòu)里,無參構(gòu)造器是永遠(yuǎn)都存在的,如果你不定義的話,它會(huì)存在;而另一方面,即使你自己手寫,編譯器也會(huì)報(bào)錯(cuò),告知你無法自己定義無參構(gòu)造器,因?yàn)闊o參構(gòu)造器是系統(tǒng)賦予的一種特殊機(jī)制,你無權(quán)更改,只能使用。
換句話說,上面這一段文字就說的是這個(gè)情況:
Person
此時(shí)是結(jié)構(gòu)的話,那么這么書寫代碼必然就會(huì)出錯(cuò)。Person
的無參構(gòu)造器是系統(tǒng)保留下來的固定存在的機(jī)制。因此請(qǐng)和類在這一點(diǎn)上進(jìn)行區(qū)分。
那么,為什么會(huì)這樣呢?值類型為啥不讓自定義無參構(gòu)造器呢?我們之前有說過一個(gè)東西,所有的系統(tǒng)類型(除了 string
和 object
),都是值類型。這些值類型都有各自的字面量書寫格式。比如 decimal
用后綴 M
標(biāo)記;int
則直接一個(gè)整數(shù)就可以了,等等這樣的東西。實(shí)際上在系統(tǒng)執(zhí)行期間,系統(tǒng)會(huì)為這些值類型單獨(dú)分配內(nèi)存空間提供變量的初始化和使用。但是,一旦我們可以自定義無參構(gòu)造器的話,我們就相當(dāng)于更改了這些值類型的初始化行為。系統(tǒng)只要默認(rèn)生成一個(gè)值類型,那么必然就得使用無參構(gòu)造器對(duì)變量的內(nèi)存空間執(zhí)行操作有一個(gè)簡單的規(guī)劃,而無參構(gòu)造器雖然里面沒有代碼,但它也必不可少。
所以,值類型的無參構(gòu)造器是我們不可改變的、固有的一種機(jī)制;這一點(diǎn)和類不一樣。接口就更不用說了,接口壓根不讓聲明構(gòu)造器,因?yàn)榻涌谑怯脕硖峁┙o別的類型的一種約束的,自身是無法實(shí)例化的。
3-2 結(jié)構(gòu)里的數(shù)據(jù)成員必須都在構(gòu)造器里給出初始化
除了結(jié)構(gòu)的無參構(gòu)造器的聲明行為不一樣以外,結(jié)構(gòu)的構(gòu)造器里,必須給出所有數(shù)據(jù)成員的初始化。這里我們要把數(shù)據(jù)成員提出來給大家說一下概念。之前也是簡單用了一下這些詞語,但因?yàn)闆]有體系化說明,所以這里說明一下。
數(shù)據(jù)成員(Data Member),指的是類和結(jié)構(gòu)里的這些實(shí)例字段。之所以稱為數(shù)據(jù)成員,是因?yàn)樗鼈儗iT用來存儲(chǔ)數(shù)據(jù),而字段本身就是類或者結(jié)構(gòu)類型里的一種成員類別,因此稱為數(shù)據(jù)成員。方法、索引器等等別的成員都不屬于數(shù)據(jù)成員,因?yàn)樗鼈兌鄶?shù)體現(xiàn)出來都是跟方法執(zhí)行的行為差不多:即在處理一些數(shù)據(jù),而不是單純的存儲(chǔ)數(shù)據(jù)。
那么,一旦我們定義出了一個(gè)結(jié)構(gòu),那么里面的這些字段就必須賦值。在類里,我們即使不給字段賦值,字段也會(huì)默認(rèn)得到一個(gè)分配的數(shù)據(jù)結(jié)果;但在結(jié)構(gòu)里,所有的字段都必須得到賦值,否則編譯器就會(huì)告訴你這么寫是錯(cuò)的。
_age
賦值,編譯器就會(huì)告訴你,_age
我們只需要追加一個(gè) : this()
的調(diào)用,就可以了。
3-3 數(shù)據(jù)成員無法使用等號(hào)直接賦值
在類里,我們可以直接在字段的末尾追加 = 數(shù)據(jù)
的語法來給字段賦值。但是在結(jié)構(gòu)里,這一點(diǎn)是不允許的。
如上代碼所示,這種語法只可能在類里出現(xiàn),結(jié)構(gòu)是無法直接對(duì)字段賦值的。至于原因……因?yàn)榻Y(jié)構(gòu)的初始化是需要通過構(gòu)造器這種嚴(yán)格處理機(jī)制對(duì)每個(gè)成員給出賦值才可保證結(jié)構(gòu)使用和實(shí)例化的安全性,而這種書寫格式因?yàn)槭翘^了構(gòu)造器對(duì)字段的初始化,所以會(huì)導(dǎo)致初始化的不安全(用戶看起來很困惑,以及編譯器對(duì)這個(gè)分析的復(fù)雜度會(huì)增加)。
總的來說,就是有點(diǎn)別扭,因此 C# 干脆不讓你這么寫了。