環境:
- 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系列委託中已經爲我們做了示範: