c#: 协变和逆变深度解析
環境:
window 10
.netcore 3.1
vs2019 16.5.1
一、為什么要有協變?
首先看下面的代碼:
還有下面的:
其實上面報錯的是同一個問題,就是你無法用List<Fruit>指向List<Apple>!
我們的疑問在于,明明是一個盛放蘋果的箱子,我們說它可以盛放水果怎么了???
下面我來說一下原因:
首先,不能根據這個類的用途去判斷,因為你無法保證List這個類一定是集合(List當然是集合,但如果是Person<T>呢,它是做什么的?只是盛放東西嗎?)。
其次,Apple繼承自Fruit沒錯,但List<Apple>和List<Fruit>壓根就沒有繼承的說法,它們是不同的類型(泛型參數類型不同也是不同的類型):Console.WriteLine(typeof(List<Apple>) == typeof(List<Fruit>));輸出為:false
所以,我們用List<Fruit>去表示List<Apple>引發報錯很正常!!!
但是,從我們程序員角度來說,這樣肯定不方便,那么有沒有解決辦法呢?
答案:有,它就是協變!
二、什么是協變?
首先,明確一下目的:我們想讓List<Fruit> list = new List<Apple>();這類代碼成立!(這行代碼肯定不成立,我說的是這類代碼)
想要達到我們的目的,肯定是要有規則的:
必須使用接口進行指向,不能使用類:
比如說:我們只能這么寫IList<Fruit> list = new List<Apple>();(雖然這樣寫也報錯),不能夠這么寫List<Fruit> list = new List<Apple>();
為什么不能使用類?因為類里面牽扯到的內容比較多,而下一條規則就說了:方法的入參不能使用泛型參數,所以為了盡量把這種約束的范圍變小一點,我們也應該在接口上加規則約束而不是直接在類上(這一點是我猜的)。
這個接口的泛型參數只能用來做接口內方法的返回值,不能用作接口內方法的參數(在泛型參數前加out關鍵字實現):
這里從兩方面說:
1.允許這個泛型參數做返回值:比如定義接口ITest<out T>,允許T作為接口內方法MethodA的返回值(T MethodA();)。在使用的時候,你用ITest<Fruit>指向ITest<Apple>,那么當調用ITest<Fruit>的方法MethodA的時候你得到的返回類型聲明是Fruit,實際上你得到的返回類型是Apple,所以一點問題沒有。
2.禁止這個泛型參數做方法的入參:比如定義接口ITest<out T>,允許T作為接口內方法MethodA的入參(void MethodA(T t);)。在使用的時候,你用ITest<Fruit>指向ITest<Apple>,那么當調用ITest<Fruit>的方法MethodA的時候你看到這個方法要求傳入一個Fruit,所以你可能傳一個orange(橙子,也繼承了Fruit)進去,但人家實際上是ITest<Apple>,要求傳入的是Apple,這樣肯定說不通!所以泛型參數禁止做方法的入參!
上面說了規則,那么下面來一個實例:
可以看到,我們按照規則在ITest的泛型參數T上加了out后,整個程序腰不酸了、腿不疼了。
事實上,微軟在集合的定義上已經考慮到了這一點,看一下IEnumerable的定義:
所以,我們像下面這樣寫也沒有錯:
講到這里,我們可以說一下什么是協變了:
假如有兩個類:A和AA,其中AA繼承自A,如果此時有一個泛型接口IC<out T>,那么可以認為IC<A>能指向IC<AA>,即:IC<AA>和IC<A>的關系看著像AA和A的關系一樣(只是看著像,并且能單方向轉換,但不是繼承!!!)。
三、什么是逆變?
逆變和協變是相對的,具體來說:
逆變的目的是:讓List<Apple> test = new List<Fruit>();這類代碼成立!(這行代碼肯定報錯,我說的是這類代碼)
你一定認為這瘋了,“說一個盛放水果的箱子盛放的是蘋果”肯定不對。
但是我們看下面的實例:
上圖中的代碼是不是顛覆了你的認知?
好吧,這就是逆變:一個可以讓你用ITest<Apple>去指向Test<Fruit>()的存在!
這里還是再說一下逆變的規則:
必須使用接口進行指向,不能使用類:
這一點和協變是一樣的。
這個接口的泛型參數只能用來做接口內方法的入參,不能用作接口內方法的返回值(在泛型參數前加in關鍵字實現):
這里從兩方面說:
1.允許這個泛型參數做方法的入參:比如定義接口ITest<in T>,允許T作為接口內方法MethodA的入參(void SetValue(T t);)。在使用的時候,你用ITest<Apple>指向ITest<Fruit>,那么當你調用ITest<Apple>的方法MethodA的時候你看到這個方法要求傳入一個Apple,實際上人家是ITest<Fruit>,人家要求傳入的是Fruit,所以這里一點問題沒有。
2.禁止這個泛型參數做方法的返回值:比如定義接口ITest<in T>,允許T作為接口內方法MethodA的返回值(T GetValue();)。在使用的時候,你用ITest<Apple>指向ITest<Fruit>,那么當調用ITest<Apple>的方法MethodA的時候你得到的返回類型聲明是Apple,但實際上人家是ITest<Fruit>,所以返回的是一個orange(橙子,也繼承了Fruit)也說不定,所以你用Apple去接收這個返回值肯定不行的,所以泛型參數禁止做方法的返回值!
四、委托內的協變和逆變
委托中的泛型參數是天然就可以支持協變或逆變中的一種的!
對這句話的理解如下:
如果你這么定義委托:public delegate T GetValue<T>();,那么它天然支持協變(因為T只用來聲明返回值),如下代碼:
如果你這么定義委托:public delegate void SetValue<T>(T t);,那么它天然支持逆變(因為T只用來做入參),如下代碼:
如果你這么定義委托,它既不支持協變,也不支持逆變:public delegate T Deal<T>(T t);(因為T即用來做入參也用來做返回值),如下代碼:
其實,在委托中為了更好的表示泛型參數是支持協變還是逆變,最好是定義的時候就用out或in參數進行聲明,比如:
public delegate T GetValue<out T>();//支持協變
public delegate void SetValue<in T>(T t);//支持逆變
微軟在Func、Action系列委托中已經為我們做了示范:
總結
以上是生活随笔為你收集整理的c#: 协变和逆变深度解析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Asp.Net Boilerplate微
- 下一篇: .net core HttpClient