第 39 講:面向?qū)ο缶幊蹋ㄊ唬航涌诘幕靖拍?/h1>
之前我們介紹了面向?qū)ο蟮膬纱蟀鍓K:類(lèi)的基本概念和成員以及類(lèi)的繼承。雖然內(nèi)容確實(shí)挺復(fù)雜,講得好像看起來(lái)很簡(jiǎn)單。但按照我這個(gè)思維方式去學(xué)習(xí),至少說(shuō)到過(guò)的語(yǔ)法點(diǎn)應(yīng)該是沒(méi)有什么大的毛病的。
下面我們進(jìn)入第三個(gè)板塊,也是面向?qū)ο蟮淖詈笠粋€(gè)部分:接口(Interface)。接口可以實(shí)現(xiàn)一些用類(lèi)無(wú)法做到的事情。下面我們通過(guò)一個(gè)引例來(lái)給大家介紹,為什么類(lèi)無(wú)法做到,只有接口可以做到。
Part 1 引例
讓我們先來(lái)思考一個(gè)例子。動(dòng)物園舉辦運(yùn)動(dòng)會(huì),大象沒(méi)有去,為什么?因?yàn)榇笙笤诒淅?/span>
好吧,正經(jīng)一點(diǎn)。動(dòng)物園舉辦運(yùn)動(dòng)會(huì),顯然我們要篩選選手才能一起參加比賽,比如飯量比賽肯定得會(huì)吃飯的動(dòng)物才可以去。顯然大象和螞蟻就不可能一起參加,因?yàn)槲浵佅鄬?duì)于大象的話(huà),就不叫作“會(huì)吃飯”,飯量小很多。
需要篩選出會(huì)吃飯的小伙伴,那么必然就需要?jiǎng)游飩儭罢娴臅?huì)吃飯”。我不管你自己吃,還是年齡小需要父母喂你吃,但起碼你得按照規(guī)則行事。可問(wèn)題在于,抽象類(lèi)能完成這樣類(lèi)似的任務(wù),因?yàn)槲覀冎恍枰屗袆?dòng)物類(lèi)型的實(shí)體從這個(gè)抽象類(lèi)派生就可以了。這個(gè)抽象類(lèi)大概設(shè)計(jì)成這樣:
Eat
表示如何吃飯。那么所有實(shí)現(xiàn)的類(lèi)型都從 Eatable
當(dāng)然,這是一個(gè)假設(shè)的實(shí)現(xiàn)方式。
好吧,別懷疑,dog food 確實(shí)是狗糧的意思;cat food 也確實(shí)是貓糧的意思。
哦對(duì),foodie 是吃貨的意思。我實(shí)在是找不到合適的詞語(yǔ)來(lái)表示“會(huì)吃東西”了。要么,就寫(xiě)成一個(gè)句子:
SomeoneWhoCanEatSomething
,但是這種說(shuō)法顯然看著很別扭,而且太長(zhǎng)了。我就找了個(gè)“吃貨”來(lái)表示這個(gè)意思。
可問(wèn)題來(lái)了。抽象類(lèi)在設(shè)計(jì)的時(shí)候有一點(diǎn)不合理,因?yàn)槌橄箢?lèi)是用來(lái)給子類(lèi)型派生提供基本模型的,但抽象類(lèi)想要表達(dá)的一個(gè)意思是“是什么東西”。比如說(shuō)之前的“圓是一種形狀”。但是 Foodie
是“會(huì)吃東西”。那么這樣的行為還有很多,比如“會(huì)做飯”、“會(huì)唱歌”、“會(huì)跳舞”啥的。
顯然,在上面這些行為里,能夠做到至少兩個(gè)情況的動(dòng)物類(lèi)型,絕對(duì)不少??稍陬?lèi)的繼承的設(shè)計(jì)一節(jié)我們說(shuō)過(guò),C# 不允許從多個(gè)類(lèi)型派生,因?yàn)橐粋€(gè)事物只能從一個(gè)類(lèi)別派生。比如這個(gè)例子里,貓咪(Cat
類(lèi)型)和狗狗(Dog
類(lèi)型)顯然應(yīng)該從動(dòng)物(Animal
類(lèi)型)派生才是最合理的設(shè)計(jì)。而“貓咪會(huì)做什么”、“狗狗會(huì)做什么”顯然不在我們這里所說(shuō)的設(shè)計(jì)范疇。因此,抽象類(lèi)就無(wú)法做到這一點(diǎn)。
那么怎么辦呢?下面我們就需要接口這種東西的依托了。
Part 2 接口的使用
接口專(zhuān)門(mén)用來(lái)解決“會(huì)做什么”的邏輯。因?yàn)椤皶?huì)做什么”這種關(guān)系是一對(duì)多的,而不是一對(duì)一的。你只有一個(gè)爸爸,但你可以唱歌、可以跳舞、可以玩游戲,可以做很多事情,它們之間可以完全沒(méi)有關(guān)聯(lián),此時(shí)設(shè)計(jì)成抽象類(lèi)就完全是不合適的。
接口的語(yǔ)法是這樣的。假設(shè)我們依舊使用“會(huì)吃東西”這個(gè)行為來(lái)作為接口的話(huà):
請(qǐng)注意接口的書(shū)寫(xiě)格式。我們?yōu)椤皶?huì)吃東西”這個(gè)行為取名叫 foodie,那么它的接口名稱(chēng)(接口名你可以類(lèi)比類(lèi)名來(lái)理解)就應(yīng)該寫(xiě)成 IFoodie
。前面這個(gè)大寫(xiě)的 I 字母是取名的規(guī)范,是一種強(qiáng)烈建議寫(xiě)上去的取名規(guī)范。為了和普通的類(lèi)作區(qū)分,故意加個(gè)字母 I 在最前面,表示它是接口。這個(gè) I 是 interface(接口)的第一個(gè)字母。
這個(gè)接口里,有一個(gè)方法叫 Eat
,可這里的 Eat
方法前面甚至連訪問(wèn)修飾符都沒(méi)有,而且直接以分號(hào)結(jié)尾,卻沒(méi)有寫(xiě) abstract
關(guān)鍵字。
實(shí)際上,public
和 abstract
是必須不寫(xiě)的。什么叫做“必須不寫(xiě)”呢?也就是說(shuō),public abstract
的組合寫(xiě)了也沒(méi)有意義,因此干脆不讓你寫(xiě)出來(lái)??蔀槭裁催@兩個(gè)關(guān)鍵字沒(méi)有意義呢?下面我們來(lái)說(shuō)一下。
首先來(lái)說(shuō)一下 abstract
。這里的 Eat
方法在接口里本身就起到了類(lèi)似抽象類(lèi)里的“派生類(lèi)必須實(shí)現(xiàn)”的作用,因此 abstract
總是去寫(xiě)一遍就有點(diǎn)麻煩,畢竟大家都知道,寫(xiě)接口的目的就是為了讓那些個(gè)派生類(lèi)去實(shí)現(xiàn)它們,所以接口里的成員默認(rèn)都是抽象的,不必寫(xiě) abstract
。那既然都不必寫(xiě) abstract
了,我又是何必讓大家每次沒(méi)完沒(méi)了地重復(fù)書(shū)寫(xiě)這個(gè)看起來(lái)都沒(méi)啥意義的 abstract
關(guān)鍵字呢?所以,C# 規(guī)定,接口里的成員是不寫(xiě) abstract
關(guān)鍵字,因?yàn)樗鼈儽旧砭鸵欢ㄊ浅橄蟮摹?/span>
至于 public
,這一點(diǎn)不是很好理解,但請(qǐng)聽(tīng)我說(shuō)。接口設(shè)計(jì)出來(lái)就是為了提供給派生類(lèi)去實(shí)現(xiàn)里面的成員的。這里所說(shuō)的實(shí)現(xiàn)(Implement),其實(shí)和抽象類(lèi)的邏輯差不多:在派生下來(lái)后,一定要寫(xiě) override
關(guān)鍵字來(lái)表明我要修改基類(lèi)型的這個(gè)同名成員的邏輯;然后在里面去修改邏輯。如果基類(lèi)型里給出的是 abstract
修飾的話(huà),那么就等于說(shuō)“我在實(shí)現(xiàn)執(zhí)行邏輯”;如果是 virtual
關(guān)鍵字的話(huà),那么就等于說(shuō)“我在重新修改執(zhí)行邏輯”。而對(duì)于接口來(lái)說(shuō),接口既然專(zhuān)門(mén)就是為了提供給子類(lèi)型實(shí)現(xiàn)用的,那么就沒(méi)有必要為這種數(shù)據(jù)類(lèi)型提供任何的訪問(wèn)修飾符,而必須是 public
。因?yàn)閯e的訪問(wèn)修飾符都比 public
要小,因此我們不論如何設(shè)計(jì)類(lèi)型其實(shí)都是可以的,畢竟它不會(huì)暴露給外界。那么既然如此的話(huà),我們就沒(méi)有必要讓接口去做這項(xiàng)任務(wù)。因此,public
是沒(méi)有必要寫(xiě)的,因?yàn)榻涌谠O(shè)計(jì)出來(lái)就是故意讓成員都是 public
的,那寫(xiě)不寫(xiě)都一樣,和 abstract
一樣的道理,就不準(zhǔn)你寫(xiě)出來(lái)了。
那么,怎么用接口呢?和類(lèi)的繼承語(yǔ)法一致,還是一個(gè)冒號(hào),后面跟上接口即可。因?yàn)榻涌诤统橄箢?lèi)有一點(diǎn)不同是“可以從多個(gè)接口派生,但只能從一個(gè)類(lèi)派生”,因此要書(shū)寫(xiě)多個(gè)接口的時(shí)候,它們之間是用逗號(hào)分隔的;而如果類(lèi)和接口同時(shí)出來(lái)的時(shí)候,先寫(xiě)基類(lèi)型(類(lèi)),然后挨個(gè)寫(xiě)接口。接口之間沒(méi)有先后順序。
注意兩個(gè)地方的語(yǔ)法。第一處是第 1 行和第 11 行的繼承寫(xiě)法:寫(xiě)的是 : Animal, IFoodie
,另外一處是實(shí)現(xiàn) Eat
方法的時(shí)候不寫(xiě) override
關(guān)鍵字。下面我們針對(duì)這兩個(gè)語(yǔ)法來(lái)說(shuō)明一下。
至于第一點(diǎn),在派生關(guān)系上,我們一定是先寫(xiě)基類(lèi)型(類(lèi)名),然后才是接口。因?yàn)榻涌诳梢杂泻芏?,我們?nèi)绻话错樞虻暮突?lèi)型混著書(shū)寫(xiě)的話(huà),很容易讓別人看著不方便,因此先寫(xiě)基類(lèi)型是一個(gè)規(guī)定。接著所有書(shū)寫(xiě)的不管是基類(lèi)型也好,接口也好,都要用逗號(hào)分隔開(kāi)。
第二點(diǎn)是不寫(xiě) override
關(guān)鍵字。因?yàn)閷?shí)現(xiàn)接口本身這個(gè)行為就是在實(shí)現(xiàn)抽象的成員。所以,我們知道實(shí)現(xiàn)成員就必然是在重寫(xiě)邏輯,因此肯定是 override
關(guān)鍵字的修飾。那么,既然必然會(huì)有的關(guān)系,何必我們要故意寫(xiě)一遍呢?所以,和 public
還有 abstract
一樣,這個(gè) override
也不讓你寫(xiě)。不過(guò),在實(shí)現(xiàn)的時(shí)候要寫(xiě) public
關(guān)鍵字,這是必須的。因?yàn)槟悴粚?xiě) public
關(guān)鍵字的話(huà),整個(gè)方法就是 void Eat()
了。在類(lèi)的基本用法里我們說(shuō)過(guò),如果不對(duì)成員書(shū)寫(xiě)訪問(wèn)修飾符,默認(rèn)是 private
。因此此時(shí)會(huì)造成語(yǔ)義沖突,即二義性(到底是用來(lái)實(shí)現(xiàn)接口才不讓寫(xiě),省略的 public
呢,還是按照默認(rèn)的訪問(wèn)修飾符 private
來(lái)看呢)。所以,public
還是必須寫(xiě)的。
Part 3 再啰嗦一點(diǎn)東西
3-1 口頭說(shuō)法的約定
在前面我們說(shuō)了接口和抽象類(lèi)的基本概念和用法的區(qū)別,那么我們來(lái)說(shuō)一下,一些口頭表述上的約定規(guī)則。
在派生關(guān)系上(就是冒號(hào)后面跟的那些類(lèi)名稱(chēng)和接口名稱(chēng)),類(lèi)被稱(chēng)為“從這個(gè)類(lèi)派生”(Extends from some class),而接口被稱(chēng)為“實(shí)現(xiàn)這個(gè)接口”(Implements some interface)。這是說(shuō)法上的約定俗成,當(dāng)然你也可以說(shuō)“從接口派生”這種說(shuō)法,不過(guò)一般我們不這么說(shuō)。
然后是名稱(chēng)上。從某個(gè)類(lèi)派生,我們稱(chēng)這個(gè)類(lèi)叫做”基類(lèi)“(Base class),而接口的話(huà),我們稱(chēng)為”基接口“(Base interface)?!被涌凇斑@個(gè)說(shuō)法其實(shí)不是正經(jīng)說(shuō)法,但是因?yàn)榻?jīng)常配合基類(lèi)一起說(shuō),所以說(shuō)多了也就當(dāng)約定俗成來(lái)用了。
比如說(shuō)前面的例子,Cat
從 IFoodie
接口派生,可以說(shuō)”Cat
類(lèi)型實(shí)現(xiàn)了 IFoodie
接口“,而”IFoodie
接口是 Cat
的一個(gè)基接口“。
3-2 好像接口能覆蓋的范圍更廣,那么為啥我們干脆不把抽象類(lèi)也用接口表示出來(lái)呢?
接口是提供一種具體實(shí)現(xiàn)用的,因此接口里是不能包含默認(rèn)成員的。所謂的默認(rèn)成員(Default Implementation Member),你可以把抽象類(lèi)里的那些 virtual
修飾的成員稱(chēng)為默認(rèn)成員。這些默認(rèn)成員是給了一種默認(rèn)的執(zhí)行實(shí)現(xiàn)的,而接口里是不允許你追加帶有默認(rèn)實(shí)現(xiàn)的成員的(畢竟連關(guān)鍵字 C# 都不讓你寫(xiě)進(jìn)去,何談 virtual
呢)。
這是第一個(gè)原因(接口不讓你書(shū)寫(xiě)默認(rèn)成員)。第二個(gè)原因是,接口里很多成員實(shí)際上是不支持的。接口專(zhuān)門(mén)是給實(shí)例成員提供的約束的,所以帶 static
修飾的所有成員都是不能寫(xiě)進(jìn)接口的。比如運(yùn)算符和轉(zhuǎn)換器。
3-3 接口的繼承
和抽象類(lèi)一樣,類(lèi)可以從類(lèi)派生,接口也一樣:接口也可以從接口派生。
在最后這個(gè)接口 IOperator
里,它從四個(gè)接口派生。接口的派生會(huì)把接口里的成員繼承下來(lái)。如果有一個(gè)類(lèi)型實(shí)現(xiàn)了 IOperator
接口,因?yàn)槔^承下來(lái)了,所以就必須實(shí)現(xiàn)這四個(gè)接口里的這四個(gè)不同的方法。
3-4 接口是引用類(lèi)型
接口和類(lèi)一樣,它是一個(gè)引用類(lèi)型。
3-5 接口的默認(rèn)訪問(wèn)修飾符
接口和類(lèi)一樣。如果訪問(wèn)修飾符不寫(xiě)的話(huà),默認(rèn)是 internal
。
3-6 接口里的成員類(lèi)型可以有哪些?
上面我們只講到了方法這一種接口成員類(lèi)型。因?yàn)槲覀冋f(shuō)接口成員類(lèi)型并不是所有的都支持,所以可能有一些成員類(lèi)型是不支持的;但是,光從理論上說(shuō)了,可能你也不知道是哪些。下面我們來(lái)說(shuō)一下。
