回想當時第一次學到協變和逆變的時候印象甚至都不是很深,也不是很能理解這個的作用和意義。
結果最近刷leetcode題的時候就遇到關於這個問題的情況。
最後解決了這個問題,以下是我解決問題的歷程。
問題的出現
就比如力扣第15題三數之和
他的返回值是IList<IList< int >> 代表意思類似二維陣列。
我第一反應就是先宣告一個List<List< int >> res 的變數來作為答案,
並理所應當的認為泛型引數是支援隱式轉換的。
但並不是這樣滴
你得寫成 List<IList< int >> 才行
這時我不禁心生疑問,難度他真的不支援隱式轉換嗎?
於是開始思考,上網查詢資料,自己研究並總結
假設可以
我們首先思考如果介面泛型引數真的支援隱式轉換會發生什麼?
首先設Dog是Animal類的子類
如果介面泛型引數真的支援隱式轉換
那麼35行不會報錯,能正常執行
因為抽象介面泛型傳入的是Animal類,
所以我們會發現36行的In方法是可以傳Animal類的,但是我們的實現類的泛型是Dog類,
這就意味著具體實現類的In方法是把這個泛型T看做Dog來處理的。
分析一下,這是因為抽象介面的In方法僅要求一個Animal類即可,
但是我們的具體實現類的In方法卻是要Dog類。
假如具體實現類的In方法會說話,他可能會說:
“我要一個Dog類,你卻傳一個Animal類給我?”
這時的In方法相當於要你子類裝父類,明顯是不合理,是具有型別安全風險的。
但是仔細觀察的話,Out方法卻是合理的
因為介面的Out方法輸出的是父類Animal
儘管實現類Out方法輸出的是Dog
但是相當於父類裝子類:Animal res = new Dog(); //是合理的
豁然開朗
所以這時候協變的作用就出來了
在泛型引數T前加入out 關鍵字
35行的報錯就解決了
使用協變(out)修飾T之後,T只能作為介面方法的返回值
所以把先前寫的In方法註釋了
如果理解了上面的協變的例子,那麼逆變就好理解了。
我們將上面例子的泛型引數調過來,
抽象介面泛型用子類Dog,具體實現類泛型用父類Animal
跟上面協變的例子同理
但這次是In方法沒問題,Out方法出現了問題。
分析一下,我們發現抽象介面的In方法要求傳入一個Dog類,
但是我們的具體實現類的In方法僅僅要求Animal類。
我要的是父類,你給我子類,這很合理,父類裝子類。
但是Out方法就有疑問了
分析一下,我們發現抽象介面的Out方法會輸出一個Dog類,
但是我們的具體實現類的Out方法僅會輸出Animal類。
我要輸出的是子類,你卻給我父類,這很合理嗎?
此時根據多型的原理,entity可能會說:
怎麼編譯之前我Out方法輸出的是一個Dog子類
但是編譯完之後我Out方法輸出的就是更抽象的一個Animal父類了?
這並意味著這兩個不能一起用,僅僅只是不能兩個同時修飾同一個泛型引數T而已
可以一個用out(協變)來修飾T1,另一個用in(逆變)來修飾T2
回到開頭
所以為什麼IList<IList< int >> 不能裝入List<List< int >>呢?
這時我們去翻看IList原始碼能發現,原來他並沒有使用協變
因為他的泛型T既要作為方法的引數輸入和輸出
所以才用不了協變,IList<IList< int >> 才不裝不了List<List< int >>
但值得注意的是IEnumerable是支援協變的
所以IEnumerable<IEnumerable< int >> 是能裝List<List< int >>的
總結
什麼是協變和逆變?
協變 允許你在泛型中使用比指定型別更具體的型別。
例如IEntity < Animal > entity = new Entity< Dog >();
它適用於返回值型別,使用 out 關鍵字標記泛型型別引數。
逆變 允許你在泛型中使用比指定型別更抽象的型別。
例如IEntity < Dog > entity = new Entity< Animal >();
它適用於引數型別,使用 in 關鍵字標記泛型型別引數。
為什麼需要協變和逆變?
在處理複雜的型別層次結構和泛型集合時,協變和逆變確保型別安全並提高程式碼的靈活性。
它能夠允許我們在不犧牲型別安全的情況下實現"屬於泛型的多型性"。