第 69 講:C# 2 之泛型(三):泛型接口及泛型參數(shù)繼承
前面的內(nèi)容告訴了大家如何使實(shí)現(xiàn)一個(gè)泛型類型(結(jié)構(gòu)和類)。今天我們來(lái)介紹一下泛型參數(shù)繼承的機(jī)制。
Part 1 創(chuàng)建一個(gè)泛型接口
先來(lái)簡(jiǎn)單的。我們來(lái)創(chuàng)建一個(gè)泛型接口類型。泛型接口類型就是一個(gè)接口類型帶有泛型參數(shù)??紤]一種情況。假設(shè)我想要提取一種集合類型,暫定名稱叫 ICollection<T>
。這個(gè) T
表示這個(gè)集合類型存儲(chǔ)的每一個(gè)元素的類型,比如數(shù)組 T[]
、列表 List<T>
的這個(gè) T
。這個(gè)接口專門提取出一個(gè)集合應(yīng)該有的一些成員,以便我在以后使用的時(shí)候,要想實(shí)現(xiàn)一個(gè)集合,直接實(shí)現(xiàn)這個(gè)接口,通過(guò) IDE 提供的自動(dòng)實(shí)現(xiàn)接口成員的功能就可以完美產(chǎn)生一系列的成員,就不必自己手寫的時(shí)候忘記東西。那么這個(gè)接口大概長(zhǎng)這樣:
是的。和類型的聲明方式完全一樣,接口也不例外:接口如果想要帶有泛型參數(shù),在聲明的時(shí)候直接按照 <T>
的形式放在后面即可。然后我們就可以在里面追加一系列的成員用來(lái)表示一個(gè)集合應(yīng)該有的東西。一般正常思路下,它得是可 foreach
迭代的、可以使用索引器的、可以增刪查成員的。其中可迭代的對(duì)象我們要求它實(shí)現(xiàn) GetEnumerator
方法即可,而索引器,我們直接給出 get
方法即可,set
方法有些時(shí)候集合也不一定非得去實(shí)現(xiàn),所以不用給出;增刪查就不多說(shuō)了。來(lái)看下寫法:
大概這樣就可以了。注意我們這里用到了兩個(gè)新的數(shù)據(jù)類型:IEnumerable<T>
和 Predicate<T>
類型。我們簡(jiǎn)單給大家說(shuō)一下這兩個(gè)類型是什么。
1-1 IEnumerable<T>
和 IEnumerator<T>
接口
IEnumerable<T>
接口是 IEnumerable
接口的泛型版本。換句話說(shuō),原本的 IEnumerable
接口也帶有 GetEnumerable
方法,但由于非泛型,因此返回值是 object
類型來(lái)兼容所有數(shù)據(jù)的;現(xiàn)在我們有了 IEnumerable<T>
后,返回值類型就可以從 IEnumerator
改成 IEnumerator<T>
了。但是,它們都是包含完全一致的方法名:GetEnumerator
。
IEnumerable<T>
的實(shí)現(xiàn)是這樣的:
IEnumerator
接口我們說(shuō)過(guò)了,它是用來(lái)啟動(dòng)迭代 foreach
而現(xiàn)在我們有了泛型版本,因此這段代碼就相當(dāng)于改成了這樣:
這樣的話,在一定程度上避免了裝箱。而早期執(zhí)行語(yǔ)句下,代碼是這樣寫的:
現(xiàn)在有了泛型,我們就可以把代碼替換成這樣了:
所以,總的來(lái)說(shuō),這個(gè)接口的意義在于泛型化接口,以盡可能避免裝箱。
接著注意一下實(shí)現(xiàn)的問(wèn)題。由于 IEnumerable<T>
是繼承接口 IEnumerable
非泛型的接口類型。那么你在實(shí)現(xiàn)了 IEnumerable<T>
接口的時(shí)候,按照接口的繼承機(jī)制,你也必須同時(shí)也實(shí)現(xiàn) IEnumerable
接口的成員。
問(wèn)題是,這兩個(gè)接口都只有一個(gè)成員,而且它僅包含這一個(gè)成員,而且好巧不巧,它們都是叫 GetEnumerator
,只有返回值不同。正是因?yàn)檫@個(gè)原因,在聲明 IEnumerable<T>
接口的時(shí)候,它里面帶的泛型版本的 GetEnumerator
方法的開(kāi)頭帶有一個(gè)修飾符 new
就是為了隱藏底層的成員。但隱藏歸隱藏,你實(shí)現(xiàn)了 IEnumerable<T>
接口也必須同時(shí)實(shí)現(xiàn)這兩個(gè)不同的方法。new
只是表示我這個(gè)方法不是從基類型繼承下來(lái)的東西,而只是巧合的時(shí)候取名遇到重名現(xiàn)象,為了規(guī)避繼承機(jī)制才需要追加 new
關(guān)鍵字的。但你既然兩個(gè)方法都得實(shí)現(xiàn),那么我必須得考慮一個(gè)問(wèn)題:實(shí)現(xiàn)都使用隱式接口實(shí)現(xiàn)就會(huì)導(dǎo)致重名而出現(xiàn)無(wú)法編譯的沖突。于是……
于是怎么樣呢?稍后我們解釋。
1-2 Predicate<T>
委托
是的。C# 靈活就靈活在,所有數(shù)據(jù)類別均可使用泛型。委托也不例外。這 Predicate<T>
委托想要代表什么方法呢?一個(gè)條件,一個(gè)以 T
類型作為判斷成員的條件。它的聲明是這樣的:
element
,是 T
類型的。然后進(jìn)行運(yùn)算后,得到一個(gè) bool
是的。Predicate<T>
泛型委托用來(lái)獲取滿足條件的成員。這個(gè) predicate
變量假設(shè)就是 Predicate<T>
類型的話,那么它就可以通過(guò)接口自帶的 Invoke
方法調(diào)用里面的回調(diào)函數(shù),去判斷是否條件成立。
1-3 整個(gè)接口里的成員
那么這么一來(lái),接口里的東西就很容易看懂了。
索引器:獲取第
index
號(hào)索引上存儲(chǔ)的元素;Add
方法:添加一個(gè)元素到集合里;Remove
方法:從集合里刪除一個(gè)指定數(shù)值的元素;Find
方法,帶T
參數(shù):找到集合里數(shù)值為element
的元素,返回它的索引;Find
方法,帶Predicate<T>
參數(shù):找到集合里滿足指定條件的元素,返回它的索引。
Part 2 從這個(gè)接口派生
2-1 泛型參數(shù)繼承
很棒。接口有了,下面我們假設(shè)實(shí)現(xiàn)了一個(gè)自己寫的類型,然后想要從這個(gè)接口派生。
注意這里的語(yǔ)法。在 SequenceList<T>
類型后帶有泛型參數(shù) T
。而我們從一個(gè)泛型接口派生。我們定義 ICollection<T>
接口的 T
表示的是集合的元素,而 SequenceList<T>
的 T
難道就不是了嗎?那很顯然是的鴨。既然這個(gè) SequenceList<T>
的 T
就是每一個(gè)元素的類型的話,那么按照接口實(shí)現(xiàn)的基本規(guī)則,我很明顯是想要讓這個(gè) T
作為泛型接口的實(shí)現(xiàn)參數(shù)才是。
可問(wèn)題是,本來(lái) T
就是泛型參數(shù)了,它自己都不確定,難道還能拿來(lái)替換別的類型里的泛型參數(shù)?是的。這就是 C# 一個(gè)新的機(jī)制:泛型參數(shù)繼承(Inheritance of Type Argument)。你仔細(xì)看看就會(huì)發(fā)現(xiàn),我 Predicate<T>
不也這么使用了嗎?
C# 認(rèn)為,你這個(gè) T
自己雖然是泛型參數(shù),但是在類型的聲明和它的大括號(hào)這段代碼里,這個(gè) T
就會(huì)自然而然地被認(rèn)為是一個(gè)普通的數(shù)據(jù)類型,只是我們不知道是什么具體的類型。這個(gè)時(shí)候我們嘗試讓 T
作為一個(gè)類型來(lái)使用,因此才能有之前的 default(T)
這些語(yǔ)法。而現(xiàn)在,在類型聲明里面,即使我們使用到了 T
作為比如 int Find(Predicate<T> predicate)
的一部分,但仍然 T
是按實(shí)際類型在看待,所以這個(gè)地方的 T
編譯器是不會(huì)管你的;而正相反地,這個(gè)寫法反而是我們以后自己實(shí)現(xiàn)泛型數(shù)據(jù)類型里,常見(jiàn)的寫法。
稍微注意一下用詞。這里的術(shù)語(yǔ)叫泛型參數(shù)繼承,用的是“繼承”,但它仍然和普通的類型繼承有所不同。泛型參數(shù)的繼承則是基于普通類型在實(shí)現(xiàn)接口或類的繼承機(jī)制的,而它帶有的泛型參數(shù)可以傳入到泛型基類或泛型基接口里當(dāng)實(shí)際泛型參數(shù)。
2-2 泛型接口的顯隱式接口實(shí)現(xiàn)
很好。下面我們來(lái)解釋一下 Part 1 里沒(méi)有解釋的問(wèn)題:由于 IEnumerable<T>
和 IEnumerable
接口里的同名但不構(gòu)成重載的方法 GetEnumerator
無(wú)法直接實(shí)現(xiàn),因?yàn)槊Q會(huì)沖突,那么怎么辦呢?
我們?nèi)匀皇褂?SequenceList<>
類型來(lái)描述和演示這個(gè)問(wèn)題的解決辦法。由于沖突是無(wú)法避免的,因此我們需要使用緊急措施:顯式接口實(shí)現(xiàn):因?yàn)轱@式接口實(shí)現(xiàn)可以規(guī)避重名方法的現(xiàn)象。我們選取一個(gè)不常用的接口,它實(shí)現(xiàn)的內(nèi)容以顯式接口實(shí)現(xiàn)的形式呈現(xiàn)出來(lái),避免沖突。那么哪個(gè)不常用呢?顯然是非泛型版本 IEnumerable
接口了。因?yàn)榉盒蜋C(jī)制在絕大多數(shù)情況下都比非泛型機(jī)制要更好,所以我們基本上可以完全放棄掉非泛型的情況,而且也能達(dá)到一致的運(yùn)行目的和效果。因此,我們使用顯式接口實(shí)現(xiàn)隱藏掉 IEnumerable
接口的成員:
我們僅需隱藏其中一個(gè)即可。當(dāng)然,兩個(gè)你都可以使用顯式接口實(shí)現(xiàn),不過(guò)一般我們?cè)谌魏握G闆r下都建議使用隱式接口實(shí)現(xiàn),因此不必顯化實(shí)現(xiàn)。
2-3 泛型接口的多態(tài)
由于你實(shí)現(xiàn)了這樣的泛型接口,因此按照多態(tài)性的規(guī)則,當(dāng)前類型是可以隱式轉(zhuǎn)換為接口類型的實(shí)例的;反之,接口類型的實(shí)例可以強(qiáng)制轉(zhuǎn)換為當(dāng)前類型的實(shí)例(如果類型匹配的話)。
反之,因?yàn)榻涌陬愋筒恢缿?yīng)該往什么類型上轉(zhuǎn)化,并且轉(zhuǎn)換可能失敗,因此需要強(qiáng)制轉(zhuǎn)換:
比如這樣。
Part 3 泛型接口的多角色實(shí)現(xiàn)
泛型類型的誕生使得接口有一種新的“黑科技”:多角色接口實(shí)現(xiàn)。
3-1 基本實(shí)現(xiàn)和“多角色”的多態(tài)性
思考一個(gè)問(wèn)題。假設(shè)我有一個(gè)集合,但這個(gè)集合擁有多種迭代行為。比如 StudentCollection
集合類型存儲(chǔ)的是一系列學(xué)生的信息,那么我們迭代可以直接迭代每一個(gè)學(xué)生的信息;但有些時(shí)候也不一定非得是迭代 Student
的實(shí)例,比如我還可以只迭代學(xué)生的 ID(string
類型)之類的。
我們理想的代碼是這樣的:
可問(wèn)題在于,方法的重載是不允許只有返回值不同的。換句話說(shuō),兩個(gè)方法如果只有返回值不同的話,兩個(gè)方法仍然不作為重載成員出現(xiàn)。因此,這樣的代碼只能存在于理論里。
別忘了?,F(xiàn)在我有了泛型接口了,我們就可以這么做了:我們同時(shí)實(shí)現(xiàn)兩次 IEnumerable<T>
接口,只是泛型參數(shù)一個(gè)是 Student
,而另外一個(gè)則是 string
,然后改一下實(shí)現(xiàn)。
還記得接口的顯式實(shí)現(xiàn)嗎?接口是可以顯式實(shí)現(xiàn)的,這就是為了避免重名成員導(dǎo)致無(wú)法繼承的問(wèn)題。而我們可以利用顯示接口實(shí)現(xiàn)的機(jī)制來(lái)達(dá)到我們以前做不到的操作。
因?yàn)榻涌?
IEnumerable<T>
從IEnumerable
接口派生,因此一定仍需要實(shí)現(xiàn)接口IEnumerable
的成員。因此最后第 9 行的代碼仍舊不可少。
請(qǐng)注意語(yǔ)法。C# 甚至允許我們同時(shí)實(shí)現(xiàn)完全相同的接口類型多次,只要泛型參數(shù)的實(shí)際類型不同就可以。這種實(shí)現(xiàn)機(jī)制稱為多角色實(shí)現(xiàn)(Multi-role Implementation)。
有了這樣的機(jī)制后,我們就可以使用 foreach
語(yǔ)句同時(shí)實(shí)現(xiàn)兩個(gè)不同的方法:
兩種寫法全部都可以了。而且它們的不同迭代對(duì)象也會(huì)自動(dòng)“路由”到對(duì)應(yīng)的 GetEnumerator
方法上,你甚至無(wú)需擔(dān)心實(shí)現(xiàn)機(jī)制沖突的問(wèn)題——C# 可以幫你區(qū)分開(kāi)不同的調(diào)用。
另外,多角色接口實(shí)現(xiàn)也不影響它自身和接口類型之間的強(qiáng)制轉(zhuǎn)換和隱式轉(zhuǎn)換。這種東西的多態(tài)和正常的多態(tài)是一樣的,只不過(guò)多角色實(shí)現(xiàn)大不了就多幾個(gè)轉(zhuǎn)換的可能罷了:
這些轉(zhuǎn)換也都是可以的。
3-2 避免多角色接口實(shí)現(xiàn)
多角色接口實(shí)現(xiàn)是一種黑科技,正是因?yàn)?C# 允許顯式接口實(shí)現(xiàn),也允許我們實(shí)現(xiàn)多次同一個(gè)接口(只要泛型參數(shù)的實(shí)際類型不同),因此才會(huì)有這樣的情況出現(xiàn)。
可問(wèn)題在于,這樣的實(shí)現(xiàn)是有問(wèn)題的。思考一下面向?qū)ο罄锏慕涌谑侨绾我环N關(guān)系:接口實(shí)現(xiàn)等于類型能做什么。類型實(shí)現(xiàn)了 IEnumerable
接口說(shuō)明類型可以迭代,類型實(shí)現(xiàn)了 IComparable
接口說(shuō)明類型可以比較大小,類型實(shí)現(xiàn)了 IEquatable
接口說(shuō)明類型的實(shí)例可以判斷是否包含相同的內(nèi)容,而類型實(shí)現(xiàn)了 ICollection<T>
接口說(shuō)明類型可以做集合可以做的事情。諸如此類。
可問(wèn)題是,我們讓一個(gè)所謂的 StudentCollection
類型實(shí)現(xiàn)了 IEnumerable<string>
,它的目的是什么呢?是不是應(yīng)該表示 StudentCollection
的實(shí)例可以按字符串類型進(jìn)行成員迭代???可是,按正常思考一般也都不會(huì)想到,StudentCollection
的迭代過(guò)程怎么會(huì)和一個(gè)字符串綁定關(guān)聯(lián)起來(lái)。因此,這樣的接口實(shí)現(xiàn)是不合面向?qū)ο蟮幕炯s定的。這個(gè)是這么實(shí)現(xiàn)接口的第一個(gè)問(wèn)題。
第二個(gè)問(wèn)題是,在稍后我們會(huì)對(duì)泛型展開(kāi)協(xié)變和逆變性質(zhì)的討論,在此我們將會(huì)討論為什么一個(gè)泛型類型是不變的,以及類型怎么轉(zhuǎn)換成協(xié)變和逆變的對(duì)應(yīng)類型,以及泛型委托類型的協(xié)變性。如果我們使用了多角色的接口實(shí)現(xiàn),會(huì)破壞安全的類型轉(zhuǎn)換,使得混淆泛型參數(shù)的協(xié)變和逆變過(guò)程。這個(gè)點(diǎn)稍微有點(diǎn)超綱,不過(guò)你先記住就行。
所以,這樣兩個(gè)原因可以說(shuō)明,我們都不建議你使用這種接口實(shí)現(xiàn)黑科技來(lái)完善你的代碼,只是存在這種機(jī)制是出于代碼的兼容性等考慮。
Part 4 泛型類型嵌套
C# 靈活的地方在于,你甚至可以嵌套使用泛型的數(shù)據(jù)類型。
如代碼所示,泛型類型仍可使用嵌套類型。不過(guò)稍微要注意一點(diǎn),就是泛型參數(shù)的問(wèn)題。
泛型參數(shù)是在這個(gè)數(shù)據(jù)類型里的任何一處地方都可以使用的,這意味著這個(gè) T
不管你里面有沒(méi)有嵌套別的類型,嵌套的什么類型,這個(gè) T
也都可以使用。那么,這樣的話就需要注意嵌套類型的泛型參數(shù)不要和外層數(shù)據(jù)類型的泛型參數(shù)重名。
如果重名,編譯器會(huì)生成一個(gè)警告信息,告訴你,我外層的數(shù)據(jù)類型已經(jīng)包含此泛型參數(shù)。如果你是無(wú)意重名的,請(qǐng)修改內(nèi)層嵌套的數(shù)據(jù)類型的泛型參數(shù)名稱,使之不要重名,否則,泛型參數(shù)重名后,嵌套數(shù)據(jù)類型的泛型參數(shù)會(huì)覆蓋掉同名泛型參數(shù),導(dǎo)致在嵌套類型里無(wú)法再看到和使用到外層的泛型參數(shù),說(shuō)白了就是隱藏掉了。
因此,我們不應(yīng)書(shū)寫代碼的時(shí)候出現(xiàn)這樣的情況,因此要么避免使用嵌套類型,要么避免使用重名的泛型參數(shù),特別是處于包含關(guān)系的情況下。