最美情侣中文字幕电影,在线麻豆精品传媒,在线网站高清黄,久久黄色视频

歡迎光臨散文網(wǎng) 會(huì)員登陸 & 注冊(cè)

第 116 講:C# 4 之接口和委托泛型參數(shù)的協(xié)變性和逆變性

2022-06-09 23:10 作者:SunnieShine  | 我要投稿

今天我們來(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)然,這里的例子里 TPet,所以這個(gè)類型在這個(gè)例子里是 IEnumerable<Pet>)。而實(shí)際上,我們是通過(guò)遍歷迭代了元素,然后逐個(gè)追加到集合之中去的。而仔細(xì)一看就會(huì)發(fā)現(xiàn),我們?cè)诘?、使用集合的期間,TPet 對(duì)吧。那么這里 foreach 循環(huán)里,這個(gè) var pet in petsvar 也是 Pet 對(duì)吧。那么 PetPet 里賦值是合理的。對(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。那么,DogPet 的派生類型,因此 Add 方法能否成功執(zhí)行呢?能。因?yàn)?Add 方法接收的是一個(gè) Pet 類型的參數(shù),而我是 Dog 類型的。Dog 不是 Pet 嗎?顯然 Dog 就是 Pet 啊。面向?qū)ο笞铋_(kāi)始我們就說(shuō)過(guò),派生類型和基類型是一種包含的關(guān)系,而派生類型和基類型可以用“什么是什么”來(lái)解釋。DogPet 類型派生,因此 DogPet,這話沒(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)形狀的面積。

接著,我們實(shí)現(xiàn)一個(gè)比較兩個(gè) Shape 的面積大小的類型,一會(huì)兒用于排序。

也都很好理解。下面我們?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)做的話:

嗯,沒(mé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ù)上的 outin 關(guān)鍵字。

2-1 泛型參數(shù)的可變性標(biāo)記符

用于泛型參數(shù)上的 inout 關(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)鍵字為啥非得用 inout 這兩個(gè)單詞,而不是別的單詞,以及實(shí)現(xiàn)規(guī)則和原理,為啥可以這么寫。

2-2 可變性標(biāo)記符只能用于接口和委托的泛型參數(shù)上

第一個(gè)要注意的點(diǎn)是,inout 只能寫在接口和委托類型里的泛型參數(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。


第 116 講:C# 4 之接口和委托泛型參數(shù)的協(xié)變性和逆變性的評(píng)論 (共 條)

分享到微博請(qǐng)遵守國(guó)家法律
突泉县| 和平县| 定边县| 泽普县| 五原县| 乌鲁木齐县| 平武县| 乾安县| 永登县| 惠水县| 德安县| 肃宁县| 合江县| 淮安市| 刚察县| 商水县| 平阴县| 东乌珠穆沁旗| 沙田区| 兰溪市| 海晏县| 绥化市| 洪湖市| 舞阳县| 邵阳县| 津市市| 昭觉县| 巴南区| 图片| 勐海县| 都江堰市| 仪陇县| 营山县| 陆川县| 布尔津县| 石棉县| 宜君县| 谷城县| 蒲城县| 灌云县| 绥芬河市|