隨Visual Studio 2010 CTP亮相的C#4和VB10,雖然在支持語言新特性方面走了相當(dāng)不一樣的兩條路:C#著重增加后期綁定和與動態(tài)語言相容的若干特性,VB10著重簡化語言和提高抽象能力;但是兩者都增加了一項功能:泛型類型的協(xié)變(covariant)和反變(contravariant)。許多人對其了解可能僅限于增加的in/out關(guān)鍵字,而對其諸多特性有所不知。下面我們就對此進行一些詳細的解釋,幫助大家正確使用該特性。
背景知識:協(xié)變和反變
很多人可能不不能很好地理解這些來自于物理和數(shù)學(xué)的名詞。我們無需去了解他們的數(shù)學(xué)定義,但是至少應(yīng)該能分清協(xié)變和反變。實際上這個詞來源于類型和類型之間的綁定。我們從數(shù)組開始理解。數(shù)組其實就是一種和具體類型之間發(fā)生綁定的類型。數(shù)組類型Int32[]就對應(yīng)于Int32這個原本的類型。任何類型T都有其對應(yīng)的數(shù)組類型T[]。那么我們的問題就來了,如果兩個類型T和U之間存在一種安全的隱式轉(zhuǎn)換,那么對應(yīng)的數(shù)組類型T[]和U[]之間是否也存在這種轉(zhuǎn)換呢?這就牽扯到了將原本類型上存在的類型轉(zhuǎn)換映射到他們的數(shù)組類型上的能力,這種能力就稱為“可變性(Variance)”。在.NET世界中,唯一允許可變性的類型轉(zhuǎn)換就是由繼承關(guān)系帶來的“子類引用->父類引用”轉(zhuǎn)換。舉個例子,就是String類型繼承自O(shè)bject類型,所以任何String的引用都可以安全地轉(zhuǎn)換為Object引用。我們發(fā)現(xiàn)String[]數(shù)組類型的引用也繼承了這種轉(zhuǎn)換能力,它可以轉(zhuǎn)換成Object[]數(shù)組類型的引用,數(shù)組這種與原始類型轉(zhuǎn)換方向相同的可變性就稱作協(xié)變(covariant)。
由于數(shù)組不支持反變性,我們無法用數(shù)組的例子來解釋反變性,所以我們現(xiàn)在就來看看泛型接口和泛型委托的可變性。假設(shè)有這樣兩個類型:TSub是TParent的子類,顯然TSub型引用是可以安全轉(zhuǎn)換為TParent型引用的。如果一個泛型接口IFoo<T>,IFoo<TSub>可以轉(zhuǎn)換為IFoo<TParent>的話,我們稱這個過程為協(xié)變,而且說這個泛型接口支持對T的協(xié)變。而如果一個泛型接口IBar<T>,IBar<TParent>可以轉(zhuǎn)換為T<TSub>的話,我們稱這個過程為反變(contravariant),而且說這個接口支持對T的反變。因此很好理解,如果一個可變性和子類到父類轉(zhuǎn)換的方向一樣,就稱作協(xié)變;而如果和子類到父類的轉(zhuǎn)換方向相反,就叫反變性。你記住了嗎?
.NET 4.0引入的泛型協(xié)變、反變性
剛才我們講解概念的時候已經(jīng)用了泛型接口的協(xié)變和反變,但在.NET 4.0之前,無論C#還是VB里都不支持泛型的這種可變性。不過它們都支持委托參數(shù)類型的協(xié)變和反變。由于委托參數(shù)類型的可變性理解起來抽象度較高,所以我們這里不準(zhǔn)備討論。已經(jīng)完全能夠理解這些概念的讀者自己想必能夠自己去理解委托參數(shù)類型的可變性。在.NET 4.0之前為什么不允許IFoo<T>進行協(xié)變或反變呢?因為對接口來講,T這個類型參數(shù)既可以用于方法參數(shù),也可以用于方法返回值。設(shè)想這樣的接口
在.NET Framework中,許多接口都僅僅將類型參數(shù)用于參數(shù)或返回值。為了使用方便,在.NET Framework 4.0里這些接口將重新聲明為允許協(xié)變或反變的版本。例如IComparable<T>就可以重新聲明成IComparable<in T>,而IEnumerable<T>則可以重新聲明為IEnumerable<out T>。不過某些接口IList<T>是不能聲明為in或out的,因此也就無法支持協(xié)變或反變。
下面提起幾個泛型協(xié)變和反變?nèi)菀缀雎缘淖⒁馐马棧?/p>
1.僅有泛型接口和泛型委托支持對類型參數(shù)的可變性,泛型類或泛型方法是不支持的。
2.值類型不參與協(xié)變或反變,IFoo<int>永遠無法變成IFoo<object>,不管有無聲明out。因為.NET泛型,每個值類型會生成專屬的封閉構(gòu)造類型,與引用類型版本不兼容。
3.聲明屬性時要注意,可讀寫的屬性會將類型同時用于參數(shù)和返回值。因此只有只讀屬性才允許使用out類型參數(shù),只寫屬性能夠使用in參數(shù)。
協(xié)變和反變的相互作用
這是一個相當(dāng)有趣的話題,我們先來看一個例子:
什么?明明是out參數(shù),我們卻要將其用于方法的參數(shù)才合法?初看起來的確會有一些驚奇。我們需要費一些周折來理解這個問題。現(xiàn)在我們考慮IBar<string>,它應(yīng)該能夠協(xié)變成IBar<object>,因為string是object的子類。因此IBar.Test(IFoo<string>)也就協(xié)變成了IBar.Test(IFoo<object>)。當(dāng)我們調(diào)用這個協(xié)變后方法時,將會傳入一個IFoo<object>作為參數(shù)。想一想,這個方法是從IBar.Test(IFoo<string>)協(xié)變來的,所以參數(shù)IFoo<object>必須能夠變成IFoo<string>才能滿足原函數(shù)的需要。這里對IFoo<object>的要求是它能夠反變成IFoo<string>!而不是協(xié)變。也就是說,如果一個接口需要對T協(xié)變,那么這個接口所有方法的參數(shù)類型必須支持對T的反變。同理我們也可以看出,如果接口要支持對T反變,那么接口中方法的參數(shù)類型都必須支持對T協(xié)變才行。這就是方法參數(shù)的協(xié)變-反變互換原則。所以,我們并不能簡單地說out參數(shù)只能用于返回值,它確實只能直接用于聲明返回值類型,但是只要一個支持反變的類型協(xié)助,out類型參數(shù)就也可以用于參數(shù)類型!換句話說,in參數(shù)除了直接聲明方法參數(shù)之外,也僅能借助支持協(xié)變的類型才能用于方法參數(shù),僅支持對T反變的類型作為方法參數(shù)也是不允許的。要想深刻理解這一概念,第一次看可能會有點繞,建議有條件的情況下多進行一些實驗。
剛才提到了方法參數(shù)上協(xié)變和反變的相互影響。那么方法的返回值會不會有同樣的問題呢?我們看如下代碼:
我們看到和剛剛正好相反,如果一個接口需要對T進行協(xié)變或反變,那么這個接口所有方法的返回值類型必須支持對T同樣方向的協(xié)變或反變。這就是方法返回值的協(xié)變-反變一致原則。也就是說,即使in參數(shù)也可以用于方法的返回值類型,只要借助一個可以反變的類型作為橋梁即可。如果對這個過程還不是特別清楚,建議也是寫一些代碼來進行實驗。至此我們發(fā)現(xiàn)協(xié)變和反變有許多有趣的特性,以至于在代碼里in和out都不像他們字面意思那么好理解。當(dāng)你看到in參數(shù)出現(xiàn)在返回值類型,out參數(shù)出現(xiàn)在參數(shù)類型時,千萬別暈倒,用本文的知識即可破解其中奧妙。
總結(jié)
經(jīng)過本文的講解,大家應(yīng)該已經(jīng)初步了解的協(xié)變和反變的含義,能夠分清協(xié)變、反變的過程。我們還討論了.NET 4.0支持泛型接口、委托的協(xié)變和反變的新功能和新語法。最后我們還套了論的協(xié)變、反變與函數(shù)參數(shù)、返回值的相互作用原理,以及由此產(chǎn)生的奇妙寫法。我希望大家看了我的文章后,能夠?qū)⑦@些知識用于泛型程序設(shè)計當(dāng)中,正確運用.NET 4.0的新增功能。祝大家使用愉快!
新聞熱點
疑難解答
圖片精選