第 33 講:面向?qū)ο缶幊蹋ㄎ澹哼\(yùn)算符重載和類型轉(zhuǎn)換器
運(yùn)算符(Operator)和類型轉(zhuǎn)換器(Cast)。
實(shí)際上類還有三個(gè)成員類型還沒有說:運(yùn)算符、類型轉(zhuǎn)換器和事件(Event),不過因?yàn)槭录@個(gè)成員需要委托類型的依賴,而委托類型我們現(xiàn)在還沒有講,難度也比較大,因此我們只能等到后面才能說了。
Part 1 運(yùn)算符系統(tǒng)和運(yùn)算符重載
要了解運(yùn)算符,就得了解運(yùn)算符在 C# 里的體系架構(gòu)。你可能會(huì)問我,運(yùn)算符不是早就講過了嗎?是的,不過……沒有那么容易。
1-1 運(yùn)算符重載的語法
要知道運(yùn)算符在使用的時(shí)候是有類型、優(yōu)先級(jí)一說。優(yōu)先級(jí)我們并未在之前提到過,因?yàn)橛?jì)算順序一般跟我們?nèi)祟惖挠?jì)算思路和思維是差不多的,因此我們不必去記住這些運(yùn)算符的順序(比如先乘除后加減之類的)。但是,運(yùn)算符實(shí)際上是和數(shù)據(jù)類型綁定的一種概念。這句話是什么意思呢?字符串的加號(hào)是拼接字符串,而數(shù)值的加法是計(jì)算求和的結(jié)果。它們表達(dá)的意義是不同的。在 C# 里,為了幫助我們理解和自定義使用運(yùn)算符的邏輯過程,運(yùn)算符重載的概念就產(chǎn)生了。運(yùn)算符的書寫格式是這樣的:
比如說,字符串的拼接運(yùn)算可通過這樣的語法寫成這樣:
這里用到了
string
的一個(gè)構(gòu)造器。C# 的那些個(gè)值類型是不需要這么書寫代碼的,因?yàn)橹苯佑凶置媪康闹С趾妥兞康馁x值,所以我們基本上碰不到那些構(gòu)造器(實(shí)際上比如說decimal
類型,就有自己的構(gòu)造器,但我們基本上碰不到它們)。字符串是一個(gè)特殊的內(nèi)置類型,因?yàn)樗且粋€(gè)引用類型。所謂的引用類型就是內(nèi)存大小不定的、總是以引用進(jìn)行傳遞的數(shù)據(jù)類型。在這個(gè)定義下,所有類聲明出來,一經(jīng)實(shí)例化(
new
語句產(chǎn)生了對(duì)象),那么它們就都是引用類型的對(duì)象。而這個(gè)引用類型有一些很特殊的構(gòu)造器,比如說傳入一個(gè)char[]
,將其轉(zhuǎn)換成一個(gè)string
這樣的代碼我們稱為運(yùn)算符重載(Operator Overloading)。這里用的重載和我們實(shí)際上說的方法的重載類似,也有一點(diǎn)不同。畢竟這里的運(yùn)算符重載是基于這里的運(yùn)算符的,所以書寫格式非常詭異。
另外順帶一提,我們需要注意的運(yùn)算符重載有如下的一些規(guī)則:
重載的所有運(yùn)算符,參數(shù)或者返回值的類型必須至少包含這個(gè)類型本身;
重載的運(yùn)算符并非全部都可以,比如
&&
、=
、? :
(條件運(yùn)算符) 這類本身就有固定含義的運(yùn)算符就不可重載;重載的運(yùn)算符有些時(shí)候是配套的,比如
+
(加法)在重載之后,那么+=
就自動(dòng)被重載,因?yàn)?a = a + b
里可寫成a += b
,而這個(gè)運(yùn)算符是可翻譯成+
計(jì)算模式的。因此你實(shí)現(xiàn)了+
的重載規(guī)則,+=
就不必單獨(dú)寫出來了(實(shí)際上也不讓你單獨(dú)為+=
這樣的復(fù)合賦值運(yùn)算符重載;重載的運(yùn)算符有些時(shí)候是成對(duì)的,比如
>
(大于)運(yùn)算符在重載之后,你就必須要把<
(小于)也一起重載了;==
和!=
也是;>=
和<=
也是;重載的
>>
(位右移運(yùn)算符)和<<
(位左移運(yùn)算符)需要傳入兩個(gè)參數(shù),而參數(shù)的右側(cè)必須是一個(gè)int
類型的參數(shù),即你只能寫成比如public static 類型 operator >>(類型 變量, int 參數(shù))
。這個(gè)int
你是不可以隨便換的;重載的運(yùn)算符必須是
public static
修飾的。
這些規(guī)則需要你記住,因?yàn)樗鼈兗s束了運(yùn)算符不能隨便亂寫和亂用。比如 %
只能是左右兩個(gè)數(shù)值計(jì)算,而你不能改變篡改定義,改成只需要一個(gè)數(shù)就參與數(shù)值計(jì)算。
這么做是有道理的。因?yàn)檫\(yùn)算符本身就不一定非要跟一個(gè)實(shí)體對(duì)象綁定起來,比如加法用的就是兩個(gè)數(shù)值的計(jì)算過程,因此定義成 static
是有道理的;而 public
是因?yàn)椋绻悴槐┞哆\(yùn)算符給外界的話,你還不如不用運(yùn)算符,因?yàn)闆]有意義。當(dāng)初你在設(shè)計(jì)數(shù)據(jù)類型,把它們寫成類的時(shí)候,肯定就是想希望這些東西外部都能用,對(duì)吧;哪有你寫個(gè)運(yùn)算符,結(jié)果只讓類內(nèi)部隨便用,出了類就不能用的道理呢?
盡管約束很多,但我們?nèi)耘f有很多很豐富的選擇和使用。這就是為了保證運(yùn)算符在執(zhí)行的時(shí)候,語義和語法模型不會(huì)變動(dòng)。比如說 +
(加號(hào)),就一定是兩個(gè)數(shù)值運(yùn)算。如果你改成了一個(gè)數(shù),加號(hào)就不叫加號(hào)了;改成三個(gè)數(shù)計(jì)算,那么 +
又怎么書寫的問題。運(yùn)算符的優(yōu)先級(jí)是系統(tǒng)規(guī)定的,所以我們不可能通過重載改變這樣的規(guī)則,比如我重載了 +
,怎么著它的優(yōu)先級(jí)也必須是在乘除號(hào)之后計(jì)算的,畢竟先乘除后加減的規(guī)則是不可撼動(dòng)的。所以,重載運(yùn)算符僅僅是為了提供一些簡(jiǎn)單的語義模型的拓展,但我們不能篡改語義模型的體系。
這里唯一需要說的重載規(guī)則,就是邏輯元運(yùn)算符。
1-2 邏輯元運(yùn)算符的重載
很奇妙。C# 完全不允許我們重載 &&
和 ||
,但我們?yōu)榱酥剌d &&
和 ||
,C# 是提供了這樣的重載機(jī)制的,那就是之前說過的邏輯元運(yùn)算符 true
和 false
了。還是需要你注意,這里的 true
和 false
不是一個(gè)布爾類型的字面量,而是真正的運(yùn)算符。
還記得我們之前講解邏輯元運(yùn)算的例子嗎?假設(shè)你在開車,開車能過馬路將同時(shí)取決于你的油箱里的油的多少,和紅綠燈的當(dāng)前亮燈的顏色。因?yàn)槲覀冋f油箱油的多少和紅綠燈亮燈的顏色我們都用了一個(gè)叫 Status
的數(shù)據(jù)類型來抽象表示了,那么它應(yīng)該是有三個(gè)數(shù)值的:Green
(OK)、Yellow
(看起來可以,但有點(diǎn)危險(xiǎn))和 Red
(完全不行)。那么,我們?nèi)绻霑鴮懘a的話,我們就可以使用這樣的語法:
假設(shè)我們要測(cè)試這個(gè)類型。
請(qǐng)注意這里的 GetFuelLaunchStatus
和 GetNavigationLaunchStatus
方法,我們并未寫出來,這僅僅表達(dá)的是“獲取油箱油的狀態(tài)”和“紅綠燈狀態(tài)”,然后返回 Status
類型的結(jié)果??梢詮倪@個(gè)代碼里看到,我們直接將 Status
的結(jié)果用 &&
連起來了。剛才才說,&&
是不可重載的,可我們?cè)趯?Status
的代碼里,確實(shí)是沒有寫類似 operator &&
之前我們說過,&&
等價(jià)于 false(a) ? a : a & b
,所以,我們只需重載 false
和 &
就可以得到 &&
的重載規(guī)則的調(diào)用邏輯;同理,||
等價(jià)于 true(a) ? a : a | b
,所以我們只需要重載 true
和 |
就可以得到 ||
的調(diào)用邏輯了。所以,我們?yōu)槭裁礇]有重載 &&
和 ||
的語法,就是因?yàn)檫@一點(diǎn)。
C# 團(tuán)隊(duì)這么設(shè)計(jì)語法(不讓重載 &&
和 ||
而是讓你重載 true
和 false
這種不是很好理解的運(yùn)算符)是有原因的。還記得我們?cè)谔幚聿紶栴愋偷?true
和 false
的計(jì)算規(guī)則嗎?拿 &&
來說,如果兩側(cè)的數(shù)值有一個(gè) false
,結(jié)果就是 false
。那么是不是可以展開 &&
的運(yùn)算規(guī)則成 false(a) ? a : a & b
呢?要是 a
是 false
(false(a)
表達(dá)式成立),那么我們直接取 a
的數(shù)值(false
這個(gè)字面量)作為表達(dá)式結(jié)果;否則,我們就得將 a & b
按照 &
的運(yùn)算規(guī)則計(jì)算出來,把得到的結(jié)果給返回出來。這個(gè)語法保證了 &&
和 ||
運(yùn)算符的短路現(xiàn)象依然在重載運(yùn)算符之后還存在。這下知道為什么 &&
里用 false
這個(gè)邏輯元運(yùn)算符了吧:因?yàn)樵诓紶栃捅磉_(dá)式計(jì)算的時(shí)候,本身就是“&&
表達(dá)式里有一方是 false
,結(jié)果就會(huì)發(fā)生短路”,這正好就對(duì)應(yīng)了邏輯元運(yùn)算符用的這個(gè) false
符號(hào)。所以它們是相通的。
Part 2 類型轉(zhuǎn)換系統(tǒng)和類型轉(zhuǎn)換器
C# 還有一個(gè)很騷的操作是,它直接允許我們自定義轉(zhuǎn)換器,將當(dāng)前類型直接轉(zhuǎn)換成別的數(shù)據(jù)類型。只要你想,什么類型都可以轉(zhuǎn)。
2-1 強(qiáng)制類型轉(zhuǎn)換器
Status
的實(shí)體對(duì)象在使用的時(shí)候可以寫成 bool condition = (bool)status
的語法。這里的 operator
后面的這個(gè) bool
就是表示 Status
類型的對(duì)象可轉(zhuǎn)換過去的類型。
語法上,我們使用 explicit
關(guān)鍵字來表達(dá)是強(qiáng)制轉(zhuǎn)換。這個(gè)詞很少用到,它在英語里是“必須有的”、“明確的”、“直率的”的意思。比如說例句:
That wasn't an explicit rule in the meeting, but I'm sure that was part of it, you know.
這倒不是在集會(huì)上必有的原則,但是我相信這是其中一個(gè)。
咳咳咳,這個(gè)不是英語課。反正大概是這么一個(gè)詞語。因?yàn)?explicit
關(guān)鍵字意思是“顯式的”,所以 explicit operator
是強(qiáng)制轉(zhuǎn)換的意思。
2-2 隱式類型轉(zhuǎn)換器
比如還是前面這個(gè)例子,我們把 explicit
改成 implicit
即可。這樣的話,類型就允許直接將 Status
類型轉(zhuǎn)換成 bool
,比如 bool condition = status
的寫法,你甚至可以不寫 (bool)
這個(gè)強(qiáng)制轉(zhuǎn)換器都行,因?yàn)樗磉_(dá)的是隱式轉(zhuǎn)換。和前面 的寫法類似,operator
?后面緊跟的這個(gè) bool
就是這個(gè) Status
類型可以轉(zhuǎn)換過去的類型。
2-3 轉(zhuǎn)換器和邏輯元運(yùn)算符的重載
前面我們說到了邏輯元運(yùn)算符的重載規(guī)則和語法??赡苣銜?huì)有這樣的疑問。我如果實(shí)現(xiàn)了 Status
到 bool
類型的隱式轉(zhuǎn)換器的話(比如上面的這個(gè)例子的寫法),那么是不是就意味著我們沒有必要實(shí)現(xiàn)那些個(gè) &
、|
、true
、false
這四個(gè)運(yùn)算符了???顯然直接一個(gè)隱式轉(zhuǎn)換器就可以解決實(shí)現(xiàn)四個(gè)運(yùn)算符的問題。
實(shí)際上,并不等價(jià)。你可以思考一點(diǎn),在我們舉例說明 Status
這個(gè)類型的實(shí)現(xiàn)規(guī)則的時(shí)候,如果油的狀態(tài)是 Yellow
的時(shí)候,而且紅綠燈也是 Yellow
這個(gè)狀態(tài)的話,按規(guī)則我們得得到 Red
的結(jié)果才對(duì),那么兩個(gè)狀態(tài)的 &&
的結(jié)果一定是 false
的;但如果按照隱式轉(zhuǎn)換的規(guī)則,兩個(gè)狀態(tài)全變成 true
了,使得運(yùn)算結(jié)果肯定是 true
,因此,并不能這么寫代碼。
Part 3 別在運(yùn)算符重載和類型轉(zhuǎn)換器里拋異常
因?yàn)檫\(yùn)算符和類型轉(zhuǎn)換器的語法規(guī)則比較特殊,所以我們不建議任何使用 C# 的朋友在執(zhí)行的代碼里拋異常。比如說這樣:
這樣實(shí)現(xiàn)代碼雖然嚴(yán)謹(jǐn),但不是良好的代碼實(shí)現(xiàn)(Ill-formed Code)。運(yùn)算符和類型轉(zhuǎn)換器的語法是寫成運(yùn)算符的符號(hào),以及類型轉(zhuǎn)換器的小括號(hào)。如果在里面拋異常的話,比如假設(shè)我們寫 bool condition = status;
這樣的隱式轉(zhuǎn)換語句,如果轉(zhuǎn)換失效,那么顯然 status
這里就會(huì)產(chǎn)生一個(gè)異常。但這僅僅是賦值語句,在別人看來就會(huì)產(chǎn)生困惑:這都哪兒跟哪兒啊,哪有賦值語句拋異常的。所以,bug 產(chǎn)生在這些地方會(huì)很隱蔽。
至此,我們就把類的所有成員都說完了一遍。