標籤:
隨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) Sub Method1(ByVal param As T) Function Method2() As T End Interface |
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(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的新增功能。祝大家使用愉快!
.NET 4.0中的泛型協變和逆變