泛型的協變與抗變(反變)

Visual Studio 2010 CTP亮相的C#4VB10,雖然在支持語言新特性方面走了相當不一樣的兩條路:C#着重增加後期綁定和與動態語言相容的若干特性,VB10着重簡化語言和提高抽象能力;但是兩者都增加了一項功能:泛型類型的協變(covariant)和抗變(contravariant)。許多人對其瞭解可能僅限於增加的in/out關鍵字,而對其諸多特性有所不知。下面我們就對此進行一些詳細的解釋,幫助大家正確使用該特性。

背景知識:協變和抗變

很多人可能不不能很好地理解這些來自於物理和數學的名詞。我們無需去了解他們的數學定義,但是至少應該能分清協變和反變。實際上這個詞來源於類型和類型之間的綁定。我們從數組開始理解。數組其實就是一種和具體類型之間發生綁定的類型。數組類型Int32[]就對應於Int32這個原本的類型。任何類型T都有其對應的數組類型T[]。那麼我們的問題就來了,如果兩個類型TU之間存在一種安全的隱式轉換,那麼對應的數組類型T[]U[]之間是否也存在這種轉換呢?這就牽扯到了將原本類型上存在的類型轉換映射到他們的數組類型上的能力,這種能力就稱爲“可變性(Variance)”。.NET世界中,原始類型唯一允許可變性的類型轉換就是由繼承關係帶來的“子類引用->父類引用”轉換。舉個例子,就是String類型繼承自Object類型,所以任何String的引用都可以安全地轉換爲Object引用。我們發現String[]數組類型的引用也繼承了這種轉換能力,它可以轉換成Object[]數組類型的引用,數組這種與原始類型轉換方向相同的可變性就稱作協變(covariant

由於數組不支持反變性,我們無法用數組的例子來解釋反變性,所以我們現在就來看看泛型接口和泛型委託的可變性。假設有這樣兩個類型:TSubTParent的子類,顯然TSub型引用是可以安全轉換爲TParent型引用的。如果一個泛型接口IFoo<T>IFoo<TSub>可以轉換爲IFoo<TParent>的話,我們稱這個過程爲協變,而且說這個泛型接口支持對T的協變。而如果一個泛型接口IBar<T>IBar<TParent>可以轉換爲T<TSub>的話,我們稱這個過程爲抗變(contravariant而且說這個接口支持對T的抗變。因此很好理解,如果一個可變性和子類到父類轉換的方向一樣,就稱作協變;而如果和子類到父類的轉換方向相反,就叫抗變。簡而言之:協變和抗變是專門針對泛型接口和泛型委託可變性的擴展,協變是指接口的泛型子類向泛型父類方向的類型轉換,抗變是指接口的泛型父類向泛型子類方向的類型轉換,如果不使用out和in標註協變和抗變,那麼這個泛型類型就是不變的。

.NET 4.0引入的泛型協變、反變性

剛纔我們講解概念的時候已經用了泛型接口的協變和反變,但在.NET 4.0之前,無論C#還是VB裏都不支持泛型的這種可變性。不過它們都支持委託參數類型的協變和反變。由於委託參數類型的可變性理解起來抽象度較高,所以我們這裏不準備討論。已經完全能夠理解這些概念的讀者自己想必能夠自己去理解委託參數類型的可變性。在.NET 4.0之前爲什麼不允許IFoo<T>進行協變或反變呢?因爲對接口來講,T這個類型參數既可以用於方法參數,也可以用於方法返回值。設想這樣的接口

interface IFoo<T>

{

    void Method1(T param);

    T Method2();

}

如果我們允許協變,從IFoo<TSub>IFoo<TParent>轉換,那麼IFoo.Method1(TSub)就會變成IFoo.Method1(TParent)。我們都知道TParent是不能安全轉換成TSub的,所以Method1這個方法就會變得不安全。同樣,如果我們允許反變IFoo<TParent>IFoo<TSub>,則TParent IFoo.Method2()方法就會變成TSub IFoo.Method2(),原本返回的TParent引用未必能夠轉換成TSub的引用,Method2的調用將是不安全的。有此可見,在沒有額外機制的限制下,泛型接口進行協變或反變都是類型不安全的。.NET 4.0改進了什麼呢?它允許在類型參數的聲明時增加一個額外的描述,以確定這個類型參數的使用範圍。我們看到,如果一個類型參數僅僅能用於函數的返回值,那麼這個類型參數就對協變相容。而相反,一個類型參數如果僅能用於方法參數,那麼這個類型參數就對反變相容。如下所示:

interface ICo<out T>
{
    T Method();
}

interface IContra<in T>
{
    void Method(T param);
}

可以看到C#4VB10都提供了大同小異的語法,Out來描述僅能作爲返回值的類型參數,用In來描述僅能作爲方法參數的類型參數。一個接口可以帶多個類型參數,這些參數可以既有In也有Out,因此我們不能簡單地說一個接口支持協變還是反變,只能說一個接口對某個具體的類型參數支持協變或反變。比如若有IBar<in T1, out T2>這樣的接口,則它對T1支持反變而對T2支持協變。舉個例子來說,IBar<object, string>能夠轉換成IBar<string, object>,這裏既有協變又有反變。

.NET Framework中,許多接口都僅僅將類型參數用於參數或返回值。爲了使用方便,在.NET Framework 4.0裏這些接口將重新聲明爲允許協變或反變的版本。例如IComparable<T>就可以重新聲明成IComparable<in T>,而IEnumerable<T>則可以重新聲明爲IEnumerable<out T>。不過某些接口IList<T>是不能聲明爲inout的,因此也就無法支持協變或反變。

下面提起幾個泛型協變和反變容易忽略的注意事項:

1.       僅有泛型接口和泛型委託支持對類型參數的可變性,泛型類或泛型方法是不支持的。

2.       值類型不參與協變或反變,IFoo<int>永遠無法變成IFoo<object>,不管有無聲明out。因爲.NET泛型,每個值類型會生成專屬的封閉構造類型,與引用類型版本不兼容。

3.       聲明屬性時要注意,可讀寫的屬性會將類型同時用於參數和返回值。因此只有只讀屬性才允許使用out類型參數,只寫屬性能夠使用in參數。

協變和反變的相互作用

這是一個相當有趣的話題,我們先來看一個例子:

interface IFoo<in T>
{
}

interface IBar<in T>
{
    void Test(IFoo<T> foo); //對嗎?
}

你能看出上述代碼有什麼問題嗎?我聲明瞭in T,然後將他用於方法的參數了,一切正常。但出乎你意料的是,這段代碼是無法編譯通過的!反而是這樣的代碼通過了編譯:

interface IFoo<in T>
{
}

interface IBar<out T>
{
    void Test(IFoo<T> foo);
}

什麼?明明是out參數,我們卻要將其用於方法的參數才合法?初看起來的確會有一些驚奇。我們需要費一些周折來理解這個問題。現在我們考慮IBar<string>,它應該能夠協變成IBar<object>,因爲stringobject的子類。因此IBar.Test(IFoo<string>)也就協變成了IBar.Test(IFoo<object>)。當我們調用這個協變後方法時,將會傳入一個IFoo<object>作爲參數。想一想,這個方法是從IBar.Test(IFoo<string>)協變來的,所以參數IFoo<object>必須能夠變成IFoo<string>才能滿足原函數的需要。這裏對IFoo<object>的要求是它能夠反變IFoo<string>!而不是協變。也就是說,如果一個接口需要對T協變,那麼這個接口所有方法的參數類型必須支持對T的反變。同理我們也可以看出,如果接口要支持對T反變,那麼接口中方法的參數類型都必須支持對T協變纔行。這就是方法參數的協變-反變互換原則所以,我們並不能簡單地說out參數只能用於返回值,它確實只能直接用於聲明返回值類型,但是隻要一個支持反變的類型協助,out類型參數就也可以用於參數類型!換句話說,in參數除了直接聲明方法參數之外,也僅能借助支持協變的類型才能用於方法參數,僅支持對T反變的類型作爲方法參數也是不允許的。要想深刻理解這一概念,第一次看可能會有點繞,建議有條件的情況下多進行一些實驗。

剛纔提到了方法參數上協變和反變的相互影響。那麼方法的返回值會不會有同樣的問題呢?我們看如下代碼:

interface IFooCo<out T>
{
}
 
interface IFooContra<in T>
{
}

interface IBar<out T1, in T2>
{
    IFooCo<T1> Test1();
    IFooContra<T2> Test2();
}

我們看到和剛剛正好相反,如果一個接口需要對T進行協變或反變,那麼這個接口所有方法的返回值類型必須支持對T同樣方向的協變或反變這就是方法返回值的協變-反變一致原則也就是說,即使in參數也可以用於方法的返回值類型,只要藉助一個可以反變的類型作爲橋樑即可。如果對這個過程還不是特別清楚,建議也是寫一些代碼來進行實驗。至此我們發現協變和反變有許多有趣的特性,以至於在代碼裏inout都不像他們字面意思那麼好理解。當你看到in參數出現在返回值類型,out參數出現在參數類型時,千萬別暈倒,用本文的知識即可破解其中奧妙。

總結

經過本文的講解,大家應該已經初步瞭解的協變和反變的含義,能夠分清協變、反變的過程。我們還討論了.NET 4.0支持泛型接口、委託的協變和反變的新功能和新語法。最後我們還套了論的協變、反變與函數參數、返回值的相互作用原理,以及由此產生的奇妙寫法。

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