第 34 講:面向?qū)ο缶幊蹋宏P(guān)于類,其它想說(shuō)的東西
對(duì)于 C# 編程語(yǔ)言來(lái)說(shuō),因?yàn)轶w系很龐大,所以學(xué)起來(lái)很麻煩。就算是正派老師也很不容易講清楚和講全知識(shí)點(diǎn),因?yàn)榧?xì)節(jié)很多以至于很容易就會(huì)漏掉一些知識(shí)點(diǎn)。當(dāng)然了,知識(shí)點(diǎn)有些是不必要說(shuō)的,或者不太重要的,所以漏掉就漏掉吧(畢竟你還可以自己查資料去學(xué),畢竟用到才去查也不妨事,這畢竟不是上課和考試,非得背下來(lái))。
本節(jié)內(nèi)容,我們就挑選一些前面沒(méi)有解答或者回答的問(wèn)題,以及一些語(yǔ)法處理機(jī)制給大家作出額外的補(bǔ)充說(shuō)明。這一節(jié)完成之后,面向?qū)ο笪覀兙屯瓿闪?1/3 了。下一節(jié)內(nèi)容我們將進(jìn)入面向?qū)ο蟮牡诙€(gè)大部分:繼承。
Part 1 構(gòu)造器的串聯(lián)調(diào)用
沒(méi)想到吧。之前我們講構(gòu)造器的時(shí)候,我們完全沒(méi)有提到構(gòu)造器之間的調(diào)用。我們只是說(shuō)了,構(gòu)造器能夠使用 new
關(guān)鍵字了調(diào)用,除此之外沒(méi)別的辦法了。
這話其實(shí)也沒(méi)錯(cuò),但這僅僅是針對(duì)于外部而言。所謂的外部,就是在使用類、要得到類的實(shí)例的時(shí)候,才會(huì)這么說(shuō)。但是,構(gòu)造器之間是可以有調(diào)用的關(guān)系的。不過(guò),因?yàn)闃?gòu)造器的特殊性,即使構(gòu)造器 A
調(diào)用了 B
,這個(gè) B
也只能在最開(kāi)始就得調(diào)用,而不能手動(dòng)調(diào)整調(diào)用位置。
我們使用之前 Person
類的那個(gè)例子給大家介紹一下構(gòu)造器的串聯(lián)調(diào)用。我們假設(shè)有這樣三個(gè)構(gòu)造器:
我們只要調(diào)用第二個(gè)構(gòu)造器的話,我們就得重復(fù)書寫 Name = name;
這樣的代碼一次(因?yàn)榈谝粋€(gè)構(gòu)造器要寫一次 Name = name;
,在第二個(gè)構(gòu)造器里又得寫一次)。
為了簡(jiǎn)化調(diào)用模型,我們可以這么寫代碼:
請(qǐng)注意第二個(gè)和第三個(gè)構(gòu)造器。我們?cè)趨?shù)表列和大括號(hào)中間插入了一段代碼:: this(參數(shù))
。這個(gè)格式就是構(gòu)造器調(diào)用構(gòu)造器的語(yǔ)法。我們傳入 name
之后,編譯器當(dāng)然就知道你要調(diào)用的肯定是第一個(gè)構(gòu)造器了,因?yàn)橹挥械谝粋€(gè)構(gòu)造器,才是只需要一個(gè) string
類型的參數(shù);而在第三個(gè)構(gòu)造器里,我們使用 this(name, age)
就意味著我們調(diào)用的構(gòu)造器一定是第二個(gè),因?yàn)橹挥械诙€(gè)構(gòu)造器的參數(shù)和類型才是和 name
參數(shù)以及 age
參數(shù)相匹配的。
因?yàn)闃?gòu)造器語(yǔ)法的特殊性,我們只能這么書寫。比如第二個(gè)構(gòu)造器的話,: this(name) { Age = age; }
這個(gè)語(yǔ)法就表示先調(diào)用第一個(gè)構(gòu)造器,先給 name
賦值;然后才是給 age
賦值。
這就體現(xiàn)出了重載的好處和用途了。重載只需要區(qū)別參數(shù)的數(shù)據(jù)類型和參數(shù)的個(gè)數(shù),就可以知道到底調(diào)用的是哪個(gè)方法??傊覀冃枰莆盏氖沁@樣兩個(gè)知識(shí)點(diǎn):
構(gòu)造器之間可以互相調(diào)用,使用的是
: this(參數(shù)表列)
的語(yǔ)法;構(gòu)造器即使能夠通過(guò)這個(gè)語(yǔ)法來(lái)達(dá)到串聯(lián)調(diào)用的過(guò)程,但執(zhí)行必須是在最開(kāi)始就執(zhí)行,這是無(wú)法改變的。
Part 2 this
引用
在我們之前的語(yǔ)法講解里,我們是沒(méi)有提到這個(gè)概念的。按照一般教材的書寫,this
引用可能早就講過(guò)了。我之所以放在最后,作為不重要的內(nèi)容來(lái)說(shuō),是因?yàn)樗拇嬖诟泻艿汀?/span>
考慮一種情況。字段被屬性封裝后,我們使用“下劃線+駝峰命名法”的命名規(guī)則來(lái)給字段命名。其實(shí),這么取名還有一個(gè)好處:避免冗余 this
引用的代碼書寫。
假設(shè),我們?yōu)樽侄稳∶臅r(shí)候,最開(kāi)頭不添加下劃線的話,我們還是拿 Person
類為例:
顯然,我們這里不一定非得賦值給 Name
屬性,因?yàn)榻o Name
屬性賦值后,還是會(huì)調(diào)用 set
方法,然后給 name
字段賦值。因此我們便可直接通過(guò) name
參數(shù)對(duì) name
字段賦值。可問(wèn)題在于,我們直接寫 name = name;
的話,我咋知道誰(shuí)是誰(shuí)呢?參數(shù)和字段都用的一個(gè)名字 name
,巧就巧在大小寫都是一樣的。這我咋辦呢?難道就不能使用這個(gè)字段名了嗎?
也不是。我們只需要在字段 name
的左邊添加語(yǔ)法 this.
來(lái)表達(dá)“這個(gè)賦值的是字段”,就可以了:
前面追加了 this.
的語(yǔ)法,我們把 this
想象成和前面索引器語(yǔ)法差不多的 this[參數(shù)]
的類似語(yǔ)法,this
把它當(dāng)成一個(gè)“萬(wàn)能替換變量”就可以了。在具體的時(shí)候,替換成具體的變量名即可。這里是因?yàn)槲覀兪窃趯?duì)這個(gè)實(shí)體的 name
字段賦值,而“這個(gè)實(shí)體”我們是無(wú)法從代碼表達(dá)出來(lái)的,于是 C# 用了一個(gè) this
關(guān)鍵字專門表達(dá)“我要賦值的是‘這個(gè)’對(duì)象”。正是因?yàn)槭恰斑@個(gè)”對(duì)象,所以我們用的單詞是 this,因?yàn)檫@個(gè)單詞剛好就是“這個(gè)”的意思。
同時(shí),所有別的實(shí)例成員(比如屬性、索引器或者沒(méi) static
修飾的方法之類)都可使用 this
關(guān)鍵字,只是系統(tǒng)一般都會(huì)推薦你把 this
給刪掉,因?yàn)闆](méi)有意義寫出來(lái),編譯器是知道的(即使不寫,本來(lái)就是賦值給這個(gè)字段、調(diào)用這個(gè)方法、使用這個(gè)屬性之類的行為)。比如說(shuō),實(shí)際上我們可以使用這樣的寫法:this.Name = name;
來(lái)表示我把參數(shù) name
賦值給屬性 Name
。但是沒(méi)有必要寫 this.
,我是這個(gè)意思。
從另一個(gè)角度說(shuō),我們使用下劃線就避免了書寫 this
的問(wèn)題,因?yàn)闆](méi)人會(huì)給參數(shù)上追加一個(gè)下劃線吧。
Part 3 嵌套類
類有一個(gè)神奇的地方在于,你可以在類里插入一個(gè)嵌套的類,就好像是循環(huán)里嵌套循環(huán)那樣。
我們注意從第 24 行開(kāi)始的代碼。這里包含了一個(gè) private class
。不是說(shuō)類不能用 private
修飾嗎?因?yàn)檫@是嵌套類。給嵌套類修飾 private
意味著這個(gè)類型不會(huì)暴露給外界的任何地方,僅在 Person
這個(gè)大括號(hào)范圍里隨便用。因此,只有嵌套類可以用 private
修飾 class
。
另外,這里帶有一個(gè) IsSame
方法,這個(gè)方法雖然修飾 public
,但因?yàn)轭愂?private
的,因此外界還是看不到它。而我們?cè)趯?==
的運(yùn)算符重載的時(shí)候,我們使用了 PersonEqualityChecker.IsSame
的靜態(tài)方法調(diào)用的語(yǔ)法,來(lái)表達(dá)方法是通過(guò)嵌套類里訪問(wèn)得到的 IsSame
方法的。
當(dāng)然,你完全不必這么去設(shè)計(jì)代碼的思維,而是直接把第 28 到第 30 行的代碼抄到 ==
的運(yùn)算符重載的執(zhí)行代碼里,直接不要嵌套類。我這么做只是讓你明白啥叫嵌套類。
嵌套類很少用到,一般在設(shè)計(jì)的時(shí)候,我們根本不會(huì)去使用嵌套類的語(yǔ)法,因?yàn)閷懫饋?lái)很丑(多套了一層大括號(hào));而且就算是給這個(gè)嵌套類使用 public
的修飾符,外界可以訪問(wèn)了,我們書寫這個(gè)嵌套類的時(shí)候,語(yǔ)法還得帶上嵌套類所在的這個(gè)類的名字 Person
,即 Person.PersonEqualityChecker.IsSame
。你看看,很丑不說(shuō),而且這樣寫,但看這句話,你也不知道前面這個(gè) Person
到底是命名空間,還是類名。
Part 4 何為良構(gòu)類型
所謂的良構(gòu)類型(Well-formed Type),說(shuō)白了,就是構(gòu)造良好的類型,也就是說(shuō),在設(shè)計(jì)類的代碼的時(shí)候,讀起來(lái)代碼很舒服的一種設(shè)計(jì)方式。
為了避免很多不必要的復(fù)雜問(wèn)題的出現(xiàn),我們會(huì)使用一些輕便、簡(jiǎn)單的語(yǔ)法來(lái)替換正統(tǒng)的復(fù)雜語(yǔ)法;一方面簡(jiǎn)化了代碼的書寫,另外一方面來(lái)說(shuō),編譯器也確實(shí)知道這些代碼都做什么,會(huì)幫我們?nèi)プ鲞@些處理過(guò)程,所以也不必?fù)?dān)心處理邏輯。
第一,類設(shè)計(jì)的時(shí)候強(qiáng)烈建議重載 ==
和 !=
運(yùn)算符,哪怕你補(bǔ)充在大小比較運(yùn)算符都行,但這倆是非常建議重載的。因?yàn)槲覀儠鴮懕容^的時(shí)候,肯定是用等號(hào)和不等號(hào)比較的時(shí)候多;但如果不重寫的話,==
和 !=
會(huì)被默認(rèn)認(rèn)為是任何數(shù)據(jù)類型都可使用的“是否指向同一塊內(nèi)存空間”的邏輯。這顯然對(duì)于比較數(shù)值來(lái)說(shuō)是沒(méi)有意義的,因此一定要重載它們。
第二,強(qiáng)烈建議在傳入引用類型作為參數(shù)的時(shí)候,驗(yàn)證參數(shù)是否為 null
。在調(diào)用方法、使用 ==
重載的時(shí)候,我們是不是也會(huì)使用傳入 null
作為比較的代碼。如果 null
在不處理的時(shí)候,null
本身又是用來(lái)表達(dá)沒(méi)有分配內(nèi)存空間,這你上哪里去比較內(nèi)部的數(shù)值?
所以,我們建議在傳入引用類型的時(shí)候,都去確認(rèn)一下 null
。
ReferenceEquals
方法是系統(tǒng)自帶的、專門表達(dá)“是否指向同一塊內(nèi)存”的方法。如果我們?cè)谥貙?==
和 !=
的時(shí)候使用比較內(nèi)存的代碼的話,因?yàn)?==
被重載掉了,因此此時(shí)再使用 ==
和 !=
的時(shí)候就會(huì)遞歸調(diào)用自己,導(dǎo)致錯(cuò)誤。所以,避免無(wú)法判斷是否內(nèi)存一致的話,我們就得用到這個(gè)默認(rèn)的系統(tǒng)方法 ReferenceEquals
了。a && b
就等價(jià)于兩個(gè)參數(shù)都是 null
;a ^ b
則等價(jià)于兩個(gè)里有一個(gè)是 null
,而另外一個(gè)不是;最后剩下的情況就是倆參數(shù)都不是 null
了,所以可使用挨個(gè)字段比較的過(guò)程。
順帶一提,因?yàn)?
ReferenceEquals
比較的是地址是不是一樣,所以你不能把倆值類型的東西放進(jìn)去,比如ReferenceEquals(3, 3)
。這樣你是得不到正確結(jié)果的。即使我們知道倆都是 3,一定是相等的,但這個(gè)方法執(zhí)行下來(lái),只可能是false
,因?yàn)樗鼈z地址不同,只是存儲(chǔ)的數(shù)據(jù)是一樣的。
第三,如果類型本身沒(méi)有關(guān)聯(lián)的話,使用自定義類型轉(zhuǎn)換器的時(shí)候請(qǐng)盡量使用顯式轉(zhuǎn)換器。比如說(shuō)我現(xiàn)在想要實(shí)現(xiàn)一個(gè)類 A
,然后去允許它直接轉(zhuǎn) int
。因?yàn)檫@倆在實(shí)現(xiàn)上好像沒(méi)啥關(guān)系,所以我們盡量都建議你這么轉(zhuǎn)換使用 explicit operator
而不是 implicit operator
。
第四,能不用 this
引用,就不要寫出來(lái)。因?yàn)閷懗鰜?lái)是沒(méi)有意義的,寫出來(lái)只會(huì)導(dǎo)致代碼看起來(lái)更復(fù)雜。
第五,請(qǐng)盡量不要使用嵌套類。原因想必我就不多說(shuō)了吧。