1. 程式人生 > >這一次,終於弄懂了協變和逆變

這一次,終於弄懂了協變和逆變

一、前言

劉大胖決定向他的師傅燈籠法師請教什麼是協變和逆變。

 

劉大胖:師傅,最近我在學習泛型介面的時候看到了協變和逆變,翻了很多資料,可還是不能完全弄懂。

燈籠法師:阿胖,你不要被這些概念弄混,編譯器可不知道你說的什麼協變逆變。這個問題,首先你得弄懂什麼叫型別的可變性。

劉大胖:可變性?

 

二、可變性

燈籠法師:對,可變性是以一種型別安全的方式,將一個物件作為另一物件來引用。雖然是可變,但其實物件的引用地址是不會變的,只是忽悠下編譯器。

劉大胖:師傅說的將一個物件作為另一物件來引用?這不就是繼承麼?

燈籠法師:是的,你可以看下面程式碼演示(C#):

 

劉大胖:哦,我理解了,由於MemoryStream繼承於Stream,所以MemoryStream的物件可以變為Stream的物件,原來我天天在接觸可變性,我竟然不知道。

燈籠法師:是的,這種轉變其實遵守了里氏替換原則,愛徒,你可還記得?

劉大胖:當然,為了面試早已爛熟於心。里氏替換原則(LSP):指的是所有引用基類的地方都可以使用其子類的物件。可是師傅,這個和協變逆變有什麼關係呢?

 

三、協變

燈籠法師:協變和逆變只是可變性的分類,主要用於泛型介面和委託中。協變逆變只是型別轉換的方向不同。我們先看下介面協變吧,假如有Apple類繼承於Fruit,如下:

 

燈籠法師:然後現在寫了一個列印水果名稱的方法,如下:

 

燈籠法師:這時如果你打算列印一些蘋果的名稱,你會怎麼寫?

劉大胖:這不是很簡單,Apple繼承自Fruit,那可以直接使用PrintFruit類了。擼了下,怎麼報錯了?程式碼如下:

 

燈籠法師:大胖,你要理清楚,雖然Apple繼承Fruit,但List<Apple>和List<Fruit>卻一點關係也沒有,如圖:

 

劉大胖:那如果這樣,豈不是要為每一種水果都要定義一個PrintFruit方法,我覺得官方不會不知道這個問題吧?

燈籠法師:這種問題,官方當然知道了,所以才有了泛型介面的協變用以支援List<Apple>自動轉為List<Fruit>。C#中使用out表示泛型引數的可協變性,List沒有out約束,所以不能協變,但它的基類IEnumable卻實現了,如圖:

 

燈籠法師:所以只要把PrintFruit的引數型別換成IEnumable就可以了,如圖:

 

劉大胖:那為什麼List<T>不能加out以支援協變呢?

燈籠法師:愛徒問的好,List繼承於IEnumable,它比IEnumable更寬泛,它支援讀和寫,但協變只能可讀,主要用於約束輸出引數。

劉大胖:好吧,我回去再消化下。師傅你再講一下什麼是逆變吧。

 

四、逆變

燈籠法師:逆變是相反的,即支援List<Fruit>轉為List<Apple>,泛型介面上新增in約束輸入引數。

劉大胖:有點懞,師傅你還是用程式碼吧!

燈籠法師:好吧,假如現在我要讓蘋果列表或桔子列表可以按名稱排序,需要一個定義一個水果比較器,此比較器能用於任何種類的水果列表,程式碼如下:

 

燈籠法師:現在給蘋果和桔子列表按名稱排序吧,程式碼如下:

 

劉大胖:師傅你別忽悠我,Sort的引數可是要具體型別的比較器的,你看程式碼:

 

燈籠法師:大胖,就這是逆變,以使得基類的泛型物件替代子類的泛型物件,主要是因為IComparer<T>中使用了in關鍵字來約束,程式碼如下:


 

五、總結

劉大胖:哦,我有點明白了,協變就是支援泛型子類自動轉泛型父類,逆變就是支援泛型父類自動轉泛型子類。

燈籠法師:也可以這麼理解,但這些轉換隻是針對編譯器,其引用地址並沒有改變。

 

翻外篇1:

協變:String =>Object

逆變:Object => String

 

翻外篇2:

燈籠法師在劉大胖走後從背後拿出手機,螢幕上顯示來不及關閉的知乎APP:

 

 更多精彩文章,可以關注我的公眾號: