隨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這個類型參數既可以用于方法參數,也可以用于方法返回值。設想這樣的接口
在.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參數。
協變和反變的相互作用
這是一個相當有趣的話題,我們先來看一個例子:
什么?明明是out參數,我們卻要將其用于方法的參數才合法?初看起來的確會有一些驚奇。我們需要費一些周折來理解這個問題?,F在我們考慮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反變的類型作為方法參數也是不允許的。要想深刻理解這一概念,第一次看可能會有點繞,建議有條件的情況下多進行一些實驗。
剛才提到了方法參數上協變和反變的相互影響。那么方法的返回值會不會有同樣的問題呢?我們看如下代碼:
我們看到和剛剛正好相反,如果一個接口需要對T進行協變或反變,那么這個接口所有方法的返回值類型必須支持對T同樣方向的協變或反變。這就是方法返回值的協變-反變一致原則。也就是說,即使in參數也可以用于方法的返回值類型,只要借助一個可以反變的類型作為橋梁即可。如果對這個過程還不是特別清楚,建議也是寫一些代碼來進行實驗。至此我們發現協變和反變有許多有趣的特性,以至于在代碼里in和out都不像他們字面意思那么好理解。當你看到in參數出現在返回值類型,out參數出現在參數類型時,千萬別暈倒,用本文的知識即可破解其中奧妙。
總結
經過本文的講解,大家應該已經初步了解的協變和反變的含義,能夠分清協變、反變的過程。我們還討論了.NET 4.0支持泛型接口、委托的協變和反變的新功能和新語法。最后我們還套了論的協變、反變與函數參數、返回值的相互作用原理,以及由此產生的奇妙寫法。我希望大家看了我的文章后,能夠將這些知識用于泛型程序設計當中,正確運用.NET 4.0的新增功能。祝大家使用愉快!
新聞熱點
疑難解答
圖片精選