.NET 4.0中的泛型的協變和逆變

轉自:http://www.cnblogs.com/jingzhongliumei/archive/2012/07/02/2573149.htmlhtml

先作點準備工做,定義兩個類:Animal類和其子類Dog類,一個泛型接口IMyInterface<T>, 他們的定義以下:spa

 

 
public class Animal
{
}
public class Dog : Animal
{
}
interface IMyInterface<T>

}
 

 

一.  協變和逆變的定義翻譯

 

從.Net Framework 4.0開始引入了一個新特性:協變與逆變,有人翻譯爲協變和反變,他們實際上所指的就是不一樣類型之間的一種轉變(Variance). 那麼具體來講什麼是協變和逆變呢?code

就拿普通類來作個類比吧,對於普通類來講,下面兩種轉換你確定不會陌生:htm

 

Animal animal = new Dog(); //類型的隱式轉換
Dog dog = (Dog)animal; //類型的強制轉換

 

與上面兩種轉換相相似,從.Net4.0開始,對於泛型接口來講,下面兩種轉換就是協變和逆變:對象

 

IMyInterface<Animal> iAnimal = null;
IMyInterface<Dog> iDog = null;
iAnimal = iDog;// 「子類」向「父類」轉換,即泛型接口的協變
iDog = iAnimal;// 「父類」向「子類」轉換,即泛型接口的逆變

 

因此若是進行簡單類比下這二者的定義的話就是:所謂協變就是泛型接口從子類向父類轉化,所謂逆變就是父類向子類轉換. 在.Net 4.0之前,是沒有協變逆變的概念的,即上面兩行代碼中的任何一行都是不容許的, 由於雖然IMyInterface<Animal>和IMyInterface<Dog>表面上看起來有點相似父類和子類的關係, 但實際上他們根本沒有任何繼承上的關係. 從.Net 4.0開始, 有條件地容許上面的協變和逆變的兩種轉化. 這個條件就是在申明接口的時候使用in或out關鍵字來修飾限制泛型參數T的使用範圍.blog

 

實際上,若是你在Visual Studio嘗試了上面的兩行協變和逆變的代碼的話, 你就會發現, 那兩行代碼根本就不能編譯經過,緣由就在於咱們並未按照語法所要求的那樣使用修飾符in或out, 可是若是咱們在泛型接口的聲明中加上了in或out限制條件來修飾泛型參數T的時候,代碼就能夠編譯經過了. 若是咱們像下面這樣聲明接口:繼承

 

interface IMyInterface<out T>
{
}

 

那麼協變(即iAnimal = iDog;)是能夠編譯經過的,逆變則不行.而若是咱們像這樣聲明接口:接口

 

 

interface IMyInterface<in T>
{
}

 

那麼逆變(即iDog = iAnimal;)是能夠編譯經過的,協變則不行.因此咱們總結起來就是: 用out來修飾泛型參數的時候則容許協變,用in來修飾泛型參數的時候則容許逆變.get

 

那麼如今你極可能會想到幾個問題,爲何.Net4.0之前不支持協變和逆變呢? 爲何.Net4.0開始微軟要引入協變逆變呢? in和out又表明了什麼意思呢?

   

二.  爲何之前不支持協變和逆變

 

注意:如下代碼只是基於假設的分析用,不能實際編譯和執行.

爲何.Net 4.0之前不支持協變和逆變呢? 仍是以本文開頭的準備工做中的兩個類一個接口爲例, 不過那個接口需修改一下,給它增長兩個方法,以下:

 

 

interface IMyInterface<T>
{
    void ShowMe(T t);
    T GetMe();
}

 

 若是容許協變的話,那麼在調用ShowMe方法的時候就可能出現問題, 請考慮以下代碼:

 

Animal animal=new Animal();
IMyInterface<Animal> iAnimal = null;
IMyInterface<Dog> iDog = null;
iAnimal = iDog;
iAnimal.ShowMe(animal); 

 

 咱們在寫iAnimal.ShowMe(animal)這行代碼的時候,Visual Studio按照IMyInterface<Animal>來進行代碼提示,以下圖所示

 

       

 

Visual Studio要咱們輸入Animal類型的對象,可是在運行時執行ShowMe方法的時候, 由於實際對象是IMyInterface<Dog>,因此實際執行的方法是ShowMe(Dog t)方法, 因此最終就有可能致使用一個Animal的實例去調用ShowMe(Dog t)方法,這顯然是錯誤的!

與上面對協變的分析相似,再來看逆變, 若是容許逆變的話,那麼在調用GetMe方法的時候就可能出現問題,代碼以下:

 

Dog animal=new Dog ();
IMyInterface<Animal> iAnimal = null;
IMyInterface<Dog> iDog = null;
iDog = iAnimal;
Dog dog=iDog.GetMe();

 

咱們在寫Dog dog=iDog.GetMe()這行代碼的時候,Visual Studio按照IMyInterface<Dog>來進行代碼提示,以下圖所示

 

       

 

Visual Studio提示返回Dog類型的對象,可是在運行時執行GetMe方法的時候, 由於實際對象是IMyInterface<Animal>,因此實際執行的方法GetMe()的返回值爲Animal, 因此最終就有可能致使用一個Animal的實例去賦值給dog,這顯然也是錯誤的!

 

經過上面的分析咱們知道, 若是容許協變的話,那麼可能會致使在有泛型輸入參數的方法在運行時出錯, 若是容許逆變的話, 則有可能致使在有泛型返回值的方法在運行時出錯. 因而可知,泛型參數T用在方法的輸出參數仍是輸入參數決定了這個泛型接口是支持協變仍是逆變. 歸根結底, 不管是協變問題仍是逆變問題都是由於這樣的一個原則: 子類能夠向父類隱式轉換可是父類不能向子類隱式轉換. 協變和逆變的問題只是這個原則變化了一個假裝外衣而已.

 

三.  爲何.Net4.0開始要引入協變逆變以及inout的用法

 

若是沒有協變的狀況下,假設有這樣一個場景,咱們須要將IEnumerable<Animal>和IEnumerable<Dog>合併成List<Animal>,代碼以下:

 

 

IEnumerable<Animal> animals = GetAnimals();
IEnumerable<Dog> dogs = GetDogs();
List<Animal> list = new List<Animal>();
list.AddRange(animals);
list.AddRange(dogs); //由於沒有協變,因此這行代碼編譯報錯

 

上面的最後一行代碼會編譯報錯,由於沒有協變,因此就不能用IEnumerable<Dog>做爲參數去調用要求參數爲IEnumerable<Animal>, 那麼咱們就不得不爲此寫一個循環, 把IEnumerable<Dog>中的Dog都提取出來,隱式轉換爲Animal再一個一個加入到list中去. 是否是以爲有點麻煩了, 明明就只是把Dog轉化爲Animal, 爲何微軟你就不能代勞一下呢?

 

由此看來, 支持協變和逆變確實能讓咱們方便地寫出更簡潔優雅的代碼, 而爲了不出現上文中所討論的錯誤,必須對泛型參數T的使用範圍進行限制, 因此引入了in和out修飾符, 從.Net4.0開始,用in來修飾泛型參數T的時候, 表示T只能用於方法的輸入參數, 此時參數T是逆變的, 若是你將T用於輸出參數的話就會編譯報錯, 同理, out所修飾的T只能用於輸出參數,此時參數T是協變的. 而且,微軟改寫了不少的原來的泛型接口, 儘量地加上了in或out修飾符, 讓這些泛型接口支持逆變或者協變, 例如將IEnumerable<T>從新聲明爲IEnumerable<out T>。. 因此在.Net4.0中, 上面的那行代碼list.AddRange(dogs)就再也不會編譯報錯了.

 

四.  總結

 

經過上面的一些簡單示例代碼和說明, 相信你們對泛型的協變和逆變應該有了一個基本的瞭解. 協變和逆變的引入讓咱們可在不一樣的泛型接口之間能夠相互賦值, 它提供了一種相似多態的特性, 增長了靈活性, 極大地方便了代碼的編寫. 可是同時也在必定程度上限制了泛型參數T使用的自由度, 被in或out修飾的泛型參數T將只能用於輸入參數或者輸出參數. 對於只須要輸入或者只須要輸出的泛型接口來講無疑是有利無弊的. 固然, 若是咱們不加修飾符in或out, 則T仍然能夠同時用於輸入參數和輸出參數的. 除了泛型接口外, 泛型委託也有協變和逆變的問題, 正如本文中所提到的那樣, 泛型接口也好,泛型委託也罷, 甚至包括逆變協變對象做爲方法參數的時候, 他們的協變逆變的問題實際上都源於一個根本的原則: 子類能夠向父類隱式轉換可是父類不能向子類隱式轉換.

相關文章
相關標籤/搜索