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的定義:
在這裏插入圖片描述
所以,我們像下面這樣寫也沒有錯:
在這裏插入圖片描述
講到這裏,我們可以說一下什麼是協變了:
假如有兩個類:AAA,其中AA繼承自A,如果此時有一個泛型接口IC<out T>,那麼可以認爲IC<A>能指向IC<AA>,即:IC<AA>IC<A>的關係看着像AAA的關係一樣(只是看着像,並且能單方向轉換,但不是繼承!!!)。

三、什麼是逆變?

逆變和協變是相對的,具體來說:
逆變的目的是:讓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即用來做入參也用來做返回值),如下代碼:
    在這裏插入圖片描述

其實,在委託中爲了更好的表示泛型參數是支持協變還是逆變,最好是定義的時候就用outin參數進行聲明,比如:
public delegate T GetValue<out T>();//支持協變
public delegate void SetValue<in T>(T t);//支持逆變

微軟在Func、Action系列委託中已經爲我們做了示範:
在這裏插入圖片描述在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章