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

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

背景知識:協變和反變

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

因爲數組不支持反變性,咱們沒法用數組的例子來解釋反變性,因此咱們如今就來看看泛型接口和泛型委託的可變性。假設有這樣兩個類型:TSub是TParent的子類,顯然TSub型引用是能夠安全轉換爲TParent型引用的。若是一個泛型接口IFoo<T>,IFoo<TSub>能夠轉換爲IFoo<TParent>的話,咱們稱這個過程爲協變,並且說這個泛型接口支持對T的協變。而若是一個泛型接口IBar<T>,IBar<TParent>能夠轉換爲T<TSub>的話,咱們稱這個過程爲反變(contravariant並且說這個接口支持對T的反變。所以很好理解,若是一個可變性和子類到父類轉換的方向同樣,就稱做協變;而若是和子類到父類的轉換方向相反,就叫反變性。你記住了嗎?函數

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

剛纔咱們講解概念的時候已經用了泛型接口的協變和反變,但在.NET 4.0以前,不管C#仍是VB裏都不支持泛型的這種可變性。不過它們都支持委託參數類型的協變和反變。因爲委託參數類型的可變性理解起來抽象度較高,因此咱們這裏不許備討論。已經徹底可以理解這些概念的讀者本身想必可以本身去理解委託參數類型的可變性。在.NET 4.0以前爲何不容許IFoo<T>進行協變或反變呢?由於對接口來說,T這個類型參數既能夠用於方法參數,也能夠用於方法返回值。設想這樣的接口性能

Interface IFoo(Of T)spa

    Sub Method1(ByVal param As T)設計

    Function Method2() As T繼承

End Interface接口

interface IFoo<T>ci

{數學

    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(Of Out T)

    Function Method() As T

End Interface

 

Interface IContra(Of In T)

    Sub Method(ByVal param As T)

End Interface

interface ICo<out T>

{

    T Method();

}

 

interface IContra<in T>

{

    void Method(T param);

}

能夠看到C#4和VB10都提供了大同小異的語法,用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>是不能聲明爲in或out的,所以也就沒法支持協變或反變。

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

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

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

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

協變和反變的相互做用

這是一個至關有趣的話題,咱們先來看一個例子:

Interface IFoo(Of In T)

 

End Interface

 

Interface IBar(Of In T)

    Sub Test(ByVal foo As IFoo(Of T)) '對嗎?

End Interface

interface IFoo<in T>

{

 

}

 

interface IBar<in T>

{

    void Test(IFoo<T> foo); //對嗎?

}

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

Interface IFoo(Of In T)

 

End Interface

 

Interface IBar(Of Out T)

    Sub Test(ByVal foo As IFoo(Of T))

End Interface

interface IFoo<in T>

{

 

}

 

interface IBar<out T>

{

    void Test(IFoo<T> foo);

}

什麼?明明是out參數,咱們卻要將其用於方法的參數才合法?初看起來的確會有一些驚奇。咱們須要費一些周折來理解這個問題。如今咱們考慮IBar<string>,它應該可以協變成IBar<object>,由於string是object的子類。所以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(Of Out T)

 

End Interface

 

Interface IFooContra(Of In T)

 

End Interface

 

Interface IBar(Of Out T1, In T2)

    Function Test1() As IFooCo(Of T1)

    Function Test2() As IFooContra(Of T2)

End Interface

interface IFooCo<out T>

{

}

 

interface IFooContra<in T>

{

}

 

interface IBar<out T1, in T2>

{

    IFooCo<T1> Test1();

    IFooContra<T2> Test2();

}

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

總結

通過本文的講解,你們應該已經初步瞭解的協變和反變的含義,可以分清協變、反變的過程。咱們還討論了.NET 4.0支持泛型接口、委託的協變和反變的新功能和新語法。最後咱們還套了論的協變、反變與函數參數、返回值的相互做用原理,以及由此產生的奇妙寫法。我但願你們看了個人文章後,可以將這些知識用於泛型程序設計當中,正確運用.NET 4.0的新增功能。祝你們使用愉快!

相關文章
相關標籤/搜索