第 116 講:C# 4 之接口和委托泛型參數(shù)的協(xié)變性和逆變性
今天我們來(lái)說(shuō)說(shuō) C# 4 的第二個(gè)新語(yǔ)法特性:關(guān)于接口和委托的泛型參數(shù)的協(xié)變性和逆變性。
這個(gè)特性實(shí)際上沒(méi)有專門的詞語(yǔ)(或者叫術(shù)語(yǔ)詞)來(lái)說(shuō)明這個(gè)點(diǎn),所以它的名字特別長(zhǎng),而且也不大好說(shuō)清楚。
本文的協(xié)變性和逆變性是一個(gè)單獨(dú)的語(yǔ)法,和 C# 2 里的委托參數(shù)逆變性和委托返回值協(xié)變性是完全不同的兩個(gè)語(yǔ)法特性,請(qǐng)注意區(qū)分和辨別。
Part 1 引例
估計(jì)各位把協(xié)變性和逆變性都忘光了吧。我們今天來(lái)舉個(gè)例子說(shuō)明一下協(xié)變性和逆變性。之前在講解委托類型參數(shù)的逆變性和返回值的協(xié)變性的時(shí)候,因?yàn)樗氖褂脠?chǎng)合并不多,而且再加上即使使用了,用戶也不怎么關(guān)心它的細(xì)節(jié)問(wèn)題,因此總是容易被忘記,所以我猜各位對(duì)這兩個(gè)玩意兒并不是很能明白。今天我們重新舉例,因?yàn)榻裉斓恼Z(yǔ)法是 C# 4 的。
1-1 協(xié)變性:AddRange
添加一組元素
考慮我們要記錄一些寵物信息,我們簡(jiǎn)單實(shí)現(xiàn)了三個(gè)類型。
別問(wèn),細(xì)節(jié)我們都沒(méi)有怎么管,因?yàn)橹皇亲鲅菔拘Ч?。一?huì)兒 new Cat()
了你就把它當(dāng)成實(shí)例化一只貓,而 new Dog()
你就當(dāng)成實(shí)例化了一只狗狗就行了。
接著,我們實(shí)現(xiàn)一個(gè)集合類型,并帶有一個(gè) AddRange
方法。這個(gè)方法的目的是添加一組元素:
也不難理解,對(duì)吧。就是包裹了一個(gè) List<T>
的元素集合。只是這里的集合我們用接口類型 ICollection<T>
接收罷了。問(wèn)題也不大。
最后,我們?cè)谥鞣椒ɡ镞@么寫:
你覺(jué)得,這樣的代碼是對(duì)的嗎?不對(duì),因?yàn)榧?xì)心一點(diǎn)的話會(huì)發(fā)現(xiàn)問(wèn)題:AddRange
接收類型是 IEnumerable<Pet>
的對(duì)象,而此時(shí)我們的 dogs
變量是 IEnumerable<Dog>
類型的。這兩個(gè)類型的泛型參數(shù)不同,所以兩個(gè)類型并不一致,畢竟元素類型不一樣怎么能說(shuō)整體的類型是一樣的呢?
那么,按道理來(lái)說(shuō),這樣的代碼是不能夠通過(guò)編譯的。但是,C# 4 的魔法允許了這樣的代碼成立。有人就說(shuō),為什么?
我們?cè)?AddRange
里使用了這個(gè)參數(shù),這個(gè)參數(shù)是 IEnumerable<T>
的類型(當(dāng)然,這里的例子里 T
是 Pet
,所以這個(gè)類型在這個(gè)例子里是 IEnumerable<Pet>
)。而實(shí)際上,我們是通過(guò)遍歷迭代了元素,然后逐個(gè)追加到集合之中去的。而仔細(xì)一看就會(huì)發(fā)現(xiàn),我們?cè)诘?、使用集合的期間,T
是 Pet
對(duì)吧。那么這里 foreach
循環(huán)里,這個(gè) var pet in pets
的 var
也是 Pet
對(duì)吧。那么 Pet
往 Pet
里賦值是合理的。對(duì)嗎?
很好。再進(jìn)一步思考一下?,F(xiàn)在我把集合改成了 IEnumerable<Dog>
之后,那么假如語(yǔ)法成立,foreach (var pet in pets)
里的 pet
是什么類型的?Dog
,對(duì)吧,因?yàn)閰?shù)雖然沒(méi)有變動(dòng),但是我們傳入的集合是從主方法里拿下來(lái)的 dogs
變量。dogs
變量是 IEnumerable<Dog>
類型的,所以如果語(yǔ)法正確的話,這里的 var
就是 Dog
。那么,Dog
是 Pet
的派生類型,因此 Add
方法能否成功執(zhí)行呢?能。因?yàn)?Add
方法接收的是一個(gè) Pet
類型的參數(shù),而我是 Dog
類型的。Dog
不是 Pet
嗎?顯然 Dog
就是 Pet
啊。面向?qū)ο笞铋_(kāi)始我們就說(shuō)過(guò),派生類型和基類型是一種包含的關(guān)系,而派生類型和基類型可以用“什么是什么”來(lái)解釋。Dog
從 Pet
類型派生,因此 Dog
是 Pet
,這話沒(méi)有毛病對(duì)吧。
既然邏輯是成立的,我們?yōu)槭裁匆拗七@種參數(shù)傳遞呢?雖然是集合。因此,C# 4 開(kāi)始,某種魔法允許了這樣的集合傳參是成立的。在早期泛型出現(xiàn)的時(shí)候,由于泛型的類型是否一致,除了看泛型本身的類型是不是一樣的,還要看泛型參數(shù)是不是也都是一樣的。而泛型,早期被設(shè)計(jì)為不變的,也就是說(shuō),任何時(shí)候都無(wú)法像是上述這樣的代碼這樣進(jìn)行集合和集合的“元素兼容性轉(zhuǎn)換”。
這個(gè)語(yǔ)法叫什么呢?泛型參數(shù)的協(xié)變性(Co-variance on Generic Arguments)。當(dāng)然了,剛才就說(shuō)過(guò)這樣的語(yǔ)法是一個(gè)魔法,也就意味著它必須滿足一定條件才可以,并不是什么時(shí)候這樣的語(yǔ)法都是隨意成立的,這就是我們要說(shuō)的“魔法”的具體原則和原理。這個(gè)我們稍后說(shuō)明。
好像,
Cat
類型實(shí)現(xiàn)出來(lái)并未在這個(gè)例子里用到過(guò)。實(shí)際上,我這里多寫出來(lái)是為了給你對(duì)比用的。實(shí)際上,如果你按照規(guī)則重新把序列從Dog
實(shí)例改成Cat
實(shí)例,一樣是成立的。這是用來(lái)對(duì)照的,只是代碼里沒(méi)有用過(guò)這個(gè)類型。
而反過(guò)來(lái)想這個(gè)問(wèn)題,如果語(yǔ)法上不支持這樣的賦值的話,那么 C# 的泛型就沒(méi)有這么靈活了,用起來(lái)有些時(shí)候也確實(shí)不夠方便。
1-2 逆變性:利用自定義對(duì)象比較排序
我們來(lái)試著寫幾個(gè)形狀類型,這些類型用于獲取得到這個(gè)對(duì)象對(duì)應(yīng)形狀的面積。
Shape
也都很好理解。下面我們?cè)賮?lái)看主方法里的使用。
你肯定會(huì)覺(jué)得代碼有點(diǎn)毛病。和協(xié)變性的例子不同,這個(gè)例子多少都覺(jué)得會(huì)有點(diǎn)別扭。有點(diǎn)別扭很正常。問(wèn)題出在哪里呢?第 8 行的排序。我們的 Array.Sort
方法是一個(gè)泛型方法。泛型方法意味著我們?cè)诜椒ㄉ弦獛Х盒蛥?shù)。一般省略掉是因?yàn)榉盒蛥?shù)會(huì)自行推斷;但是為了這里語(yǔ)法嚴(yán)謹(jǐn)和能夠更直觀一些,我就把泛型參數(shù)寫出來(lái)了。可問(wèn)題是,此時(shí)泛型參數(shù)寫出來(lái)就出問(wèn)題了。第二個(gè)參數(shù)是傳入一個(gè)自定義比較器。這個(gè)比較器是用來(lái)排序兩個(gè)自定義對(duì)象的大小的。可問(wèn)題在于什么呢,在于對(duì)象是 ShapeAreaComparer
類型的。這個(gè)類型的代碼寫著,它實(shí)現(xiàn)的接口類型是 IComparer<Shape>
(實(shí)際上代碼里也確實(shí)按 Shape
來(lái)參與的比較)。而 Array.Sort
方法的用法,排序用的第二個(gè)參數(shù),類型應(yīng)該是與之匹配的 IComparer<Circle>
才對(duì)。這里有點(diǎn)別扭的地方就在于,此時(shí)的 Shape
傳入的這個(gè)泛型參數(shù)的實(shí)際類型反而比 Circle
這個(gè)本應(yīng)該支持的泛型參數(shù)的實(shí)際類型要“大”。
這是合理的嗎?實(shí)際上也是合理的。為什么呢?
假設(shè)我們這個(gè) Array.Sort
排序用的是冒泡排序法這個(gè)老熟人來(lái)做的話:
注意我是為了讓你更清楚直觀理解而說(shuō)的是假設(shè),實(shí)際上不是哈。實(shí)際上用的是快速排序??焖倥判虻拇a理解起來(lái)比較困難,所以用我們的老熟人,讓各位更清楚直觀地感受到比較兩個(gè)實(shí)例大小關(guān)系的代碼。
注意到第 7 行代碼,我們可以發(fā)現(xiàn),這里的 comparer
對(duì)象發(fā)揮了它的作用:比較兩個(gè)對(duì)象的大小關(guān)系。如果兩個(gè)對(duì)象的比較結(jié)果 >= 0
,就說(shuō)明前面比后面大,或者兩個(gè)對(duì)象一樣大。因?yàn)榉盒蛥?shù) T
是隨便啥類型都可以,所以這個(gè)類型并不直接包含比較大小的方法在里面。因此你直接對(duì)兩個(gè) T
使用 CompareTo
之類的方法,或者是大小于運(yùn)算符的話,是肯定不可以的。為了解決這個(gè)問(wèn)題,我們才想到了用比較器對(duì)象來(lái)做。
那么,回頭來(lái)看排序方法。我們傳入的比較器是 ShapeAreaComparer
類型的,它兼容的類型實(shí)例是 Shape
類型的。而我們要求比較大小的實(shí)例類型要寬泛。原來(lái)接收的是 Circle
類型的實(shí)例,這里要求的是 Shape
類型的實(shí)例。你說(shuō),這樣傳參合適嗎?仍然是合適的。 正因?yàn)?Shape
類型比 Circle
要寬,所以傳參進(jìn)去之后會(huì)做一次隱式轉(zhuǎn)換,或者換句話說(shuō) Circle
可以使用轉(zhuǎn)化為 Shape
的多態(tài)原則,再或者說(shuō),Shape
類型因?yàn)榕缮隽?Circle
,所以類型上是兼容的。
反正不管怎么說(shuō)吧,這種肯定是正確的傳參過(guò)程,因此編譯器并不應(yīng)該禁止掉這樣的賦值。而這也是針對(duì)于泛型參數(shù)的語(yǔ)法,所以早期仍然是不允許的。從 C# 4 開(kāi)始,這種魔法允許了這樣的過(guò)程成立。于是,你可以對(duì)一個(gè) Circle
類型使用這個(gè)看起來(lái)不太合理但正確的語(yǔ)法了。
這種語(yǔ)法稱為泛型參數(shù)的逆變性(Contra-variance on Generic Arguments)。和前文一樣,它也依賴于某種魔法才可以成立,并不是隨意兩個(gè)泛型類型參數(shù)不同都可以成立的。
Part 2 探討“魔法”:滿足前文協(xié)變和逆變性的條件
前文兩個(gè)例子使得我們可以成功使用這樣的看起來(lái)不合規(guī)但邏輯合理的語(yǔ)法來(lái)完成我們想要的操作。那么,我之前說(shuō)它們是魔法,那么這樣的魔法是怎么做的呢?到底什么樣的賦值過(guò)程,滿足什么樣的條件,前文的代碼才可以真正正確、嚴(yán)謹(jǐn)和正常呢?
請(qǐng)仔細(xì)對(duì)比前面兩個(gè)引例,有什么相似之處,你發(fā)現(xiàn)了嗎?對(duì)了。這兩個(gè)類型都是帶入了一個(gè)方法來(lái)產(chǎn)生了協(xié)變性和逆變性的過(guò)程,而這個(gè)方法產(chǎn)生這個(gè)過(guò)程的點(diǎn),恰好都在于一個(gè)泛型接口類型上。比如第一個(gè)例子,AddRange
的實(shí)現(xiàn)要求傳參是 IEnumerable<T>
接口類型,它是泛型的;而第二個(gè)例子,排序方法 Sort
要求的第二個(gè)參數(shù)類型 IComparer<T>
也是接口類型,也是泛型的。
那么,我們就在這個(gè)泛型接口上找“魔法”。分別打開(kāi) IEnumerable<T>
和 IComparer<T>
的元數(shù)據(jù),你會(huì)發(fā)現(xiàn)魔法顯現(xiàn)了出來(lái):


泛型參數(shù) T
上竟標(biāo)了兩個(gè)關(guān)鍵字!其中 IEnumerable<T>
是標(biāo)的 out
,而 IComparer<T>
上標(biāo)的是 in
。
之前說(shuō)過(guò)一次,不過(guò)各位可能已經(jīng)忘掉了。這里的元數(shù)據(jù)里,
T
上面有個(gè)問(wèn)號(hào)標(biāo)記。這個(gè)語(yǔ)法是 C# 9 才有的,稱為泛型參數(shù)在沒(méi)有任何約束情況下表示它可為null
的標(biāo)記。換句話說(shuō),這兩個(gè)參數(shù)T
都是允許null
接收的(當(dāng)然,前提是這個(gè)T
是一個(gè)支持null
作為取值的類型,比如可空值類型和引用類型)。在 C# 8 以前(比如現(xiàn)在我們講解教程期間),是不是null
是無(wú)法從語(yǔ)法上表達(dá)出來(lái)的。當(dāng)然,這里要把可空值類型給排除在外,因?yàn)樗玫木褪沁@個(gè)?
標(biāo)記。不過(guò),在引用類型里,因?yàn)橐妙愋偷哪J(rèn)數(shù)值就是null
,所以它在任何時(shí)候都是可能包含null
值的,因此養(yǎng)成習(xí)慣會(huì)經(jīng)常在方法執(zhí)行的剛開(kāi)始對(duì)所有的引用類型進(jìn)行判空操作(比如寫一句if (obj == null) throw new ArgumentNullException()
這樣的語(yǔ)法,或者是把obj == null
改成ReferenceEquals(obj, null)
之類的。不過(guò),這里不是給各位扯這個(gè)語(yǔ)法的,因?yàn)樗湍壳暗慕榻B內(nèi)容來(lái)說(shuō)已經(jīng)超綱了,所以就不再繼續(xù)詳述這個(gè)點(diǎn)的內(nèi)容。在 C# 9 以前,這個(gè)問(wèn)號(hào)標(biāo)記
?
你直接當(dāng)它不存在就可以了,不會(huì)影響代碼的執(zhí)行和理解。
啊這……這倆關(guān)鍵字有什么講究嗎?接下來(lái)就來(lái)說(shuō)說(shuō),泛型參數(shù)上的 out
和 in
關(guān)鍵字。
2-1 泛型參數(shù)的可變性標(biāo)記符
用于泛型參數(shù)上的 in
和 out
關(guān)鍵字稱為可變性標(biāo)記符。別問(wèn),我也不知道英語(yǔ)是啥,因?yàn)檫@個(gè)詞是我自己為了好介紹所以自創(chuàng)的??勺冃灾傅氖菂f(xié)變性和逆變性,它倆因?yàn)閭鲄⑦^(guò)程之中,泛型參數(shù)的實(shí)際類型會(huì)產(chǎn)生變化,所以稱為可變性;標(biāo)記符就不用多說(shuō)了。而其中:
in
關(guān)鍵字:泛型參數(shù)支持逆變性;out
關(guān)鍵字:泛型參數(shù)支持協(xié)變性。
只有這兩個(gè)關(guān)鍵字要使用到,而且這個(gè)規(guī)則是永遠(yuǎn)成立的,因此你可以背下來(lái)。但是我不建議你背,因?yàn)槿菀谆煜?,特別是剛學(xué)的時(shí)候分不清楚的時(shí)候,容易記混,別問(wèn)我為啥知道這些,我也是這么過(guò)來(lái)的。
下面說(shuō)說(shuō),關(guān)鍵字為啥非得用 in
和 out
這兩個(gè)單詞,而不是別的單詞,以及實(shí)現(xiàn)規(guī)則和原理,為啥可以這么寫。
2-2 可變性標(biāo)記符只能用于接口和委托的泛型參數(shù)上
第一個(gè)要注意的點(diǎn)是,in
和 out
只能寫在接口和委托類型里的泛型參數(shù)(如果它有泛型參數(shù)的話)上面,其它類型不行。比如自己定義的類類型 Collection<T>
里,T
是不能寫 in
或者 out
的。當(dāng)然,泛型結(jié)構(gòu)也不行。說(shuō)了只能是接口和委托了。
為啥呢?因?yàn)橹挥形泻徒涌诓艔V泛具有“傳參轉(zhuǎn)換”的用法。眾所周知,我們目前學(xué)到過(guò)的自定義類型語(yǔ)法有五種:
結(jié)構(gòu)(
struct
)類(
class
)委托(
delegate
)接口(
interface
)枚舉(
enum
)
其中,只有前四種支持泛型,枚舉不支持泛型(枚舉自己都是值類型,都直接和整數(shù)進(jìn)行轉(zhuǎn)換了,還要它成泛型干嘛)。于是,就還剩下四種:
泛型結(jié)構(gòu)
泛型類
泛型委托
泛型接口
其中,結(jié)構(gòu)是不能自定義派生和繼承關(guān)系的,它們默認(rèn)走 ValueType
類型派生;而類則是可以定義派生和繼承關(guān)系。但是,C# 的面向?qū)ο蟮捏w系限制了派生關(guān)系必須是一個(gè)類型如果有自定義的基類型的話,只能有一個(gè)。因此,它還是相當(dāng)于從“這棵樹(shù)”上一條龍下來(lái)的部分,因此也不夠通用。
所以,只剩下兩個(gè)類型了:
泛型委托
泛型接口
這兩個(gè)類型具有廣泛的使用。任何類型都可以實(shí)現(xiàn)接口,不管你是值類型還是引用類型;而且接口自己還可以從接口派生得到。所以接口從這個(gè)角度來(lái)說(shuō),使用特別廣泛;另外,委托從 C# 1 就有,當(dāng)時(shí),我們是使用方法名稱當(dāng)參數(shù)傳遞進(jìn)去進(jìn)行實(shí)例化委托的實(shí)例的;而到了 C# 2 和 3 開(kāi)始支持匿名函數(shù)和 lambda 表達(dá)式之后,事情就變得有趣了起來(lái):由于廣泛推崇使用匿名函數(shù)和 lambda 表達(dá)式,因此這樣的調(diào)用過(guò)程越來(lái)越多樣化;而在 LINQ 里,我們知道關(guān)鍵字語(yǔ)法會(huì)被轉(zhuǎn)換為方法調(diào)用,而方法調(diào)用的參數(shù)也都是一個(gè)一個(gè)的委托類型實(shí)例,因此傳入的關(guān)鍵字寫法的操作也都會(huì)被處理為合適的 lambda 表達(dá)式。所以 LINQ 里對(duì)委托的使用也可以算進(jìn)來(lái)。
正是因?yàn)檫@樣的原因,接口和委托才具備這種可變性的使用條件:因?yàn)樗鼈儾攀钦嬲撵`活變化的類型。因此,C# 4 對(duì)這兩種類型支持了泛型參數(shù)的可變性。
再嚴(yán)謹(jǐn)一點(diǎn)的話。泛型類類型的泛型參數(shù)不支持可變性,其實(shí)還有別的原因。因?yàn)榉盒皖愵愋偷姆盒蛥?shù)并不會(huì)使用成前文這樣的調(diào)用,所以對(duì)類類型支持這樣的語(yǔ)法,是不太能夠體現(xiàn)出轉(zhuǎn)換的意義所在的。
2-3 滿足協(xié)變性和逆變性的真正條件
要想正確使用上述關(guān)鍵字,不是隨便怎么寫都行的。要真是我就加一個(gè)關(guān)鍵字到泛型參數(shù)上去就行的話,仍然不能體現(xiàn)出語(yǔ)法的嚴(yán)謹(jǐn),以及轉(zhuǎn)換的嚴(yán)謹(jǐn)。
還記得 C# 2 的委托參數(shù)逆變和返回值協(xié)變嗎?這個(gè)特性里至少都約定了參數(shù)是支持逆變性,而返回值支持協(xié)變性的規(guī)則。意思就是說(shuō),因?yàn)榉祷刂抵С值氖菂f(xié)變性,因此你不能生搬硬套讓返回值支持逆變性;反過(guò)來(lái),參數(shù)也不能用協(xié)變性。這是從語(yǔ)法上限制的。
還記得原因嗎?為什么參數(shù)支持的是逆變性,而返回值支持的是協(xié)變性?我們使用動(dòng)漫《夏日重現(xiàn)》和《只有我不在的城市》的時(shí)間回溯技能,回到那一講的內(nèi)容去!
唰~

呃,不中二病了。說(shuō)說(shuō)原因吧。因?yàn)閰?shù)體現(xiàn)的效果是“抽象提取”。基類型和子類型來(lái)對(duì)比的話,顯然基類型包含的東西,子類型應(yīng)該都有,因?yàn)檫@是派生關(guān)系,是面向?qū)ο罄锏囊淮蠡咎匦浴?/span>
那么,基類型里既然都有的話,那我寫進(jìn)參數(shù)里去,基類型能使,那么子類型也能使:到時(shí)候調(diào)用的時(shí)候,就即使是支持子類型的成員,也仍然是支持的,因?yàn)樽宇愋痛_實(shí)包含它們。所以,這種限制是沒(méi)有意義和必要的,于是 C# 2 就放開(kāi)了限制。
而對(duì)于今天的例子,邏輯其實(shí)是類似的。因?yàn)槲覀冊(cè)谑褂脜?shù)的時(shí)候,參數(shù)傳入的內(nèi)容即使是基類型的實(shí)例,但基類型的東西子類型它都有啊。你想想剛才的 IComparer<>
接口實(shí)現(xiàn)的自定義比較器類型。那個(gè)類型我們就只需要用到 Area
屬性比較大小。而這個(gè)屬性 Shape
里就有。所以我基類型就有的東西,子類型自然也有。所以,我們壓根不需要用子類型 Circle
這種更精確的類型進(jìn)行判斷,而是直接拿出它的基類型的判斷規(guī)則來(lái)判斷就 OK。這也是逆變性的基本思想,因此,泛型參數(shù)的逆變性,表示的是泛型參數(shù)的實(shí)際類型用于參數(shù)的時(shí)候。這句話有點(diǎn)繞,注意區(qū)分兩個(gè)“參數(shù)”的概念:泛型參數(shù)的參數(shù)說(shuō)的是泛型類型 T
的是基類型,而第二個(gè)參數(shù)說(shuō)的是方法里的實(shí)際傳參。
而正是因?yàn)檫@個(gè)原因,我們才把泛型參數(shù)用 in
進(jìn)行修飾。原因就在于,它是用來(lái)當(dāng)方法參數(shù)的,方法參數(shù)是“傳入”,那不就是 pass into 的這個(gè) in 嗎?所以,要想泛型參數(shù)支持逆變性,那么標(biāo)記的關(guān)鍵字是 in
。
協(xié)變性呢?對(duì)咯,泛型參數(shù)的實(shí)際類型用于返回值的時(shí)候。返回值的使用場(chǎng)景是將數(shù)據(jù)反饋給外界使用的這么一種存在。考慮前文的 IEnumerable<>
接口類型的例子,它的返回值體現(xiàn)在哪里呢?foreach
循環(huán)的迭代變量上。因?yàn)檫@個(gè)接口里包含了一個(gè) IEnumerator<>
接口作返回值的 GetEnumerator
的無(wú)參方法。這個(gè)方法的返回值是一個(gè)迭代器對(duì)象,但實(shí)際上,只要支持這個(gè)方法我們就可以使用 foreach
循環(huán);而反過(guò)來(lái)說(shuō)也成立:能使用 foreach
循環(huán)的話,那么這個(gè)類型是肯定包含 GetEnumerator
的無(wú)參方法的。
所以說(shuō),它倆是相輔相成的存在。所以我們說(shuō)這個(gè)接口的使用和返回值,體現(xiàn)在了 foreach
循環(huán)上。foreach
循環(huán)迭代對(duì)象的類型,應(yīng)該是 IEnumerable<T>
接口類型的泛型參數(shù) T
這個(gè)類型。而這個(gè)類型在我們使用過(guò)程之中是將其追加到集合 _list
里。這個(gè)集合是 T
類型作為元素的存在。那么,如果我這個(gè) T
類型如果是一個(gè)范圍寬泛的基類型的話,那么我即使傳入子類型,也是可以兼容和傳入進(jìn)去的,因?yàn)闀?huì)有一次隱式的轉(zhuǎn)換過(guò)程。而這個(gè)過(guò)程,用的是 foreach
的迭代變量,是這個(gè) IEnumerable<>
迭代過(guò)程的返回值結(jié)果來(lái)進(jìn)行迭代的。
詳細(xì)一點(diǎn)的話,我們可以完整把 foreach
寫成 while
循環(huán)的方式:
這里的 Current
屬性就是使用了 enumerator
這個(gè)返回值的核心所在。它類型較小,所以傳入 Add
方法需要的參數(shù)類型較大的情況,才會(huì)成立;因?yàn)檩^小傳入給較大的地方使用才會(huì)正確發(fā)生隱式轉(zhuǎn)換,是安全的轉(zhuǎn)換。反過(guò)來(lái)肯定就不對(duì)了:較大類型傳入給較小的地方,就好比你一個(gè)大胖子走小路,然后卡在路中間了一樣的道理。
所以,這么使用結(jié)果,是合理的。它的過(guò)程,也是正常的小轉(zhuǎn)大的過(guò)程,所以是協(xié)變性。因此,整個(gè)對(duì)比來(lái)看,泛型參數(shù)的協(xié)變性,表示的是泛型參數(shù)的實(shí)際類型用于返回值的時(shí)候。正是因?yàn)檫@個(gè)原因,它是“返回出去給外界”的過(guò)程,所以是 return outside 的這個(gè) out 單詞。因此,協(xié)變性用的是 out
關(guān)鍵字來(lái)修飾泛型參數(shù)。
那么,總結(jié)一下:
泛型參數(shù)的協(xié)變性用
out
關(guān)鍵字修飾,用于實(shí)際類型只用在返回值上的情況;泛型參數(shù)的逆變性用
in
關(guān)鍵字修飾,用于實(shí)際類型只用在參數(shù)上的情況。
所以,再次回到開(kāi)頭查看兩個(gè)類型的元數(shù)據(jù)就可以發(fā)現(xiàn),確實(shí)是滿足的:


你看,IEnumerable<out T>
的這個(gè) T
是不是在接口類型里只用在了返回值上?而再次看 IComparer<in T>
的 T
是不是只用在了參數(shù)上?
記住了嗎?
2-4 最后順帶一提
前文介紹都是按泛型接口來(lái)說(shuō)的,因?yàn)樗容^好舉例子。但是,這個(gè)語(yǔ)法支持的范圍不只是泛型接口,還可以是泛型委托。所以不要忘了這一點(diǎn)。
不過(guò),是泛型委托的時(shí)候要注意,它和 C# 2 里的參數(shù)逆變性和返回值協(xié)變性是兩種完全不同的可變性語(yǔ)法特性,一定要注意區(qū)分:C# 2 的語(yǔ)法不依賴于泛型參數(shù),任何時(shí)候都可以,只要它是委托類型就 OK;而 C# 4 的這個(gè)語(yǔ)法是針對(duì)于泛型參數(shù)來(lái)說(shuō)的。
Part 3 .NET 庫(kù)里的類型有哪些支持了這個(gè)呢?
實(shí)際上有很多。列個(gè)表給大家看看。

這個(gè)表在 C# 的里有列舉出來(lái):
https://docs.microsoft.com/en-us/dotnet/standard/generics/covariance-and-contravariance
就不多展開(kāi)說(shuō)明了。如果對(duì)于庫(kù) API 不是很熟悉的小伙伴,自己看元數(shù)據(jù)去吧。
至此,我們就把這個(gè)語(yǔ)法給大家說(shuō)明清楚了。下一講是 C# 4 里的最后一個(gè)語(yǔ)法:可選和命名參數(shù)。它的內(nèi)容比較容易去說(shuō)明,所以內(nèi)容不多,C# 4 就結(jié)束了,然后是 C# 5。