詳解.NET 4.0中的泛型協(xié)變(covariant)和反變(contravariant)
隨Visual Studio 2010 CTP亮相的C#4和VB10,雖然在支持語(yǔ)言新特性方面走了相當(dāng)不一樣的兩條路:C#著重增加后期綁定和與動(dòng)態(tài)語(yǔ)言相容的若干特性,VB10著重簡(jiǎn)化語(yǔ)言和提高抽象能力;但是兩者都增加了一項(xiàng)功能:泛型類(lèi)型的協(xié)變(covariant)和反變(contravariant)。許多人對(duì)其了解可能僅限于增加的in/out關(guān)鍵字,而對(duì)其諸多特性有所不知。下面我們就對(duì)此進(jìn)行一些詳細(xì)的解釋?zhuān)瑤椭蠹艺_使用該特性。
背景知識(shí):協(xié)變和反變
很多人可能不不能很好地理解這些來(lái)自于物理和數(shù)學(xué)的名詞。我們無(wú)需去了解他們的數(shù)學(xué)定義,但是至少應(yīng)該能分清協(xié)變和反變。實(shí)際上這個(gè)詞來(lái)源于類(lèi)型和類(lèi)型之間的綁定。我們從數(shù)組開(kāi)始理解。數(shù)組其實(shí)就是一種和具體類(lèi)型之間發(fā)生綁定的類(lèi)型。數(shù)組類(lèi)型Int32[]就對(duì)應(yīng)于Int32這個(gè)原本的類(lèi)型。任何類(lèi)型T都有其對(duì)應(yīng)的數(shù)組類(lèi)型T[]。那么我們的問(wèn)題就來(lái)了,如果兩個(gè)類(lèi)型T和U之間存在一種安全的隱式轉(zhuǎn)換,那么對(duì)應(yīng)的數(shù)組類(lèi)型T[]和U[]之間是否也存在這種轉(zhuǎn)換呢?這就牽扯到了將原本類(lèi)型上存在的類(lèi)型轉(zhuǎn)換映射到他們的數(shù)組類(lèi)型上的能力,這種能力就稱(chēng)為“可變性(Variance)”。在.NET世界中,唯一允許可變性的類(lèi)型轉(zhuǎn)換就是由繼承關(guān)系帶來(lái)的“子類(lèi)引用->父類(lèi)引用”轉(zhuǎn)換。舉個(gè)例子,就是String類(lèi)型繼承自O(shè)bject類(lèi)型,所以任何String的引用都可以安全地轉(zhuǎn)換為Object引用。我們發(fā)現(xiàn)String[]數(shù)組類(lèi)型的引用也繼承了這種轉(zhuǎn)換能力,它可以轉(zhuǎn)換成Object[]數(shù)組類(lèi)型的引用,數(shù)組這種與原始類(lèi)型轉(zhuǎn)換方向相同的可變性就稱(chēng)作協(xié)變(covariant)。
由于數(shù)組不支持反變性,我們無(wú)法用數(shù)組的例子來(lái)解釋反變性,所以我們現(xiàn)在就來(lái)看看泛型接口和泛型委托的可變性。假設(shè)有這樣兩個(gè)類(lèi)型:TSub是TParent的子類(lèi),顯然TSub型引用是可以安全轉(zhuǎn)換為T(mén)Parent型引用的。如果一個(gè)泛型接口IFoo<T>,IFoo<TSub>可以轉(zhuǎn)換為IFoo<TParent>的話,我們稱(chēng)這個(gè)過(guò)程為協(xié)變,而且說(shuō)這個(gè)泛型接口支持對(duì)T的協(xié)變。而如果一個(gè)泛型接口IBar<T>,IBar<TParent>可以轉(zhuǎn)換為T(mén)<TSub>的話,我們稱(chēng)這個(gè)過(guò)程為反變(contravariant),而且說(shuō)這個(gè)接口支持對(duì)T的反變。因此很好理解,如果一個(gè)可變性和子類(lèi)到父類(lèi)轉(zhuǎn)換的方向一樣,就稱(chēng)作協(xié)變;而如果和子類(lèi)到父類(lèi)的轉(zhuǎn)換方向相反,就叫反變性。你記住了嗎?
.NET 4.0引入的泛型協(xié)變、反變性
剛才我們講解概念的時(shí)候已經(jīng)用了泛型接口的協(xié)變和反變,但在.NET 4.0之前,無(wú)論C#還是VB里都不支持泛型的這種可變性。不過(guò)它們都支持委托參數(shù)類(lèi)型的協(xié)變和反變。由于委托參數(shù)類(lèi)型的可變性理解起來(lái)抽象度較高,所以我們這里不準(zhǔn)備討論。已經(jīng)完全能夠理解這些概念的讀者自己想必能夠自己去理解委托參數(shù)類(lèi)型的可變性。在.NET 4.0之前為什么不允許IFoo<T>進(jìn)行協(xié)變或反變呢?因?yàn)閷?duì)接口來(lái)講,T這個(gè)類(lèi)型參數(shù)既可以用于方法參數(shù),也可以用于方法返回值。設(shè)想這樣的接口
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();
}
如果我們?cè)试S協(xié)變,從IFoo<TSub>到IFoo<TParent>轉(zhuǎn)換,那么IFoo.Method1(TSub)就會(huì)變成IFoo.Method1(TParent)。我們都知道TParent是不能安全轉(zhuǎn)換成TSub的,所以Method1這個(gè)方法就會(huì)變得不安全。同樣,如果我們?cè)试S反變IFoo<TParent>到IFoo<TSub>,則TParent IFoo.Method2()方法就會(huì)變成TSub IFoo.Method2(),原本返回的TParent引用未必能夠轉(zhuǎn)換成TSub的引用,Method2的調(diào)用將是不安全的。有此可見(jiàn),在沒(méi)有額外機(jī)制的限制下,接口進(jìn)行協(xié)變或反變都是類(lèi)型不安全的。.NET 4.0改進(jìn)了什么呢?它允許在類(lèi)型參數(shù)的聲明時(shí)增加一個(gè)額外的描述,以確定這個(gè)類(lèi)型參數(shù)的使用范圍。我們看到,如果一個(gè)類(lèi)型參數(shù)僅僅能用于函數(shù)的返回值,那么這個(gè)類(lèi)型參數(shù)就對(duì)協(xié)變相容。而相反,一個(gè)類(lèi)型參數(shù)如果僅能用于方法參數(shù),那么這個(gè)類(lèi)型參數(shù)就對(duì)反變相容。如下所示:
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都提供了大同小異的語(yǔ)法,用Out來(lái)描述僅能作為返回值的類(lèi)型參數(shù),用In來(lái)描述僅能作為方法參數(shù)的類(lèi)型參數(shù)。一個(gè)接口可以帶多個(gè)類(lèi)型參數(shù),這些參數(shù)可以既有In也有Out,因此我們不能簡(jiǎn)單地說(shuō)一個(gè)接口支持協(xié)變還是反變,只能說(shuō)一個(gè)接口對(duì)某個(gè)具體的類(lèi)型參數(shù)支持協(xié)變或反變。比如若有IBar<in T1, out T2>這樣的接口,則它對(duì)T1支持反變而對(duì)T2支持協(xié)變。舉個(gè)例子來(lái)說(shuō),IBar<object, string>能夠轉(zhuǎn)換成IBar<string, object>,這里既有協(xié)變又有反變。
在.NET Framework中,許多接口都僅僅將類(lèi)型參數(shù)用于參數(shù)或返回值。為了使用方便,在.NET Framework 4.0里這些接口將重新聲明為允許協(xié)變或反變的版本。例如IComparable<T>就可以重新聲明成IComparable<in T>,而IEnumerable<T>則可以重新聲明為IEnumerable<out T>。不過(guò)某些接口IList<T>是不能聲明為in或out的,因此也就無(wú)法支持協(xié)變或反變。
下面提起幾個(gè)泛型協(xié)變和反變?nèi)菀缀雎缘淖⒁馐马?xiàng):
1.僅有泛型接口和泛型委托支持對(duì)類(lèi)型參數(shù)的可變性,泛型類(lèi)或泛型方法是不支持的。
2.值類(lèi)型不參與協(xié)變或反變,IFoo<int>永遠(yuǎn)無(wú)法變成IFoo<object>,不管有無(wú)聲明out。因?yàn)?NET泛型,每個(gè)值類(lèi)型會(huì)生成專(zhuān)屬的封閉構(gòu)造類(lèi)型,與引用類(lèi)型版本不兼容。
3.聲明屬性時(shí)要注意,可讀寫(xiě)的屬性會(huì)將類(lèi)型同時(shí)用于參數(shù)和返回值。因此只有只讀屬性才允許使用out類(lèi)型參數(shù),只寫(xiě)屬性能夠使用in參數(shù)。
協(xié)變和反變的相互作用
這是一個(gè)相當(dāng)有趣的話題,我們先來(lái)看一個(gè)例子:
Interface IFoo(Of In T)
End Interface
Interface IBar(Of In T)
Sub Test(ByVal foo As IFoo(Of T)) '對(duì)嗎?
End Interface
interface IFoo<in T>
{
}
interface IBar<in T>
{
void Test(IFoo<T> foo); //對(duì)嗎?
}
你能看出上述代碼有什么問(wèn)題嗎?我聲明了in T,然后將他用于方法的參數(shù)了,一切正常。但出乎你意料的是,這段代碼是無(wú)法編譯通過(guò)的!反而是這樣的代碼通過(guò)了編譯:
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參數(shù),我們卻要將其用于方法的參數(shù)才合法?初看起來(lái)的確會(huì)有一些驚奇。我們需要費(fèi)一些周折來(lái)理解這個(gè)問(wèn)題?,F(xiàn)在我們考慮IBar<string>,它應(yīng)該能夠協(xié)變成IBar<object>,因?yàn)閟tring是object的子類(lèi)。因此IBar.Test(IFoo<string>)也就協(xié)變成了IBar.Test(IFoo<object>)。當(dāng)我們調(diào)用這個(gè)協(xié)變后方法時(shí),將會(huì)傳入一個(gè)IFoo<object>作為參數(shù)。想一想,這個(gè)方法是從IBar.Test(IFoo<string>)協(xié)變來(lái)的,所以參數(shù)IFoo<object>必須能夠變成IFoo<string>才能滿足原函數(shù)的需要。這里對(duì)IFoo<object>的要求是它能夠反變成IFoo<string>!而不是協(xié)變。也就是說(shuō),如果一個(gè)接口需要對(duì)T協(xié)變,那么這個(gè)接口所有方法的參數(shù)類(lèi)型必須支持對(duì)T的反變。同理我們也可以看出,如果接口要支持對(duì)T反變,那么接口中方法的參數(shù)類(lèi)型都必須支持對(duì)T協(xié)變才行。這就是方法參數(shù)的協(xié)變-反變互換原則。所以,我們并不能簡(jiǎn)單地說(shuō)out參數(shù)只能用于返回值,它確實(shí)只能直接用于聲明返回值類(lèi)型,但是只要一個(gè)支持反變的類(lèi)型協(xié)助,out類(lèi)型參數(shù)就也可以用于參數(shù)類(lèi)型!換句話說(shuō),in參數(shù)除了直接聲明方法參數(shù)之外,也僅能借助支持協(xié)變的類(lèi)型才能用于方法參數(shù),僅支持對(duì)T反變的類(lèi)型作為方法參數(shù)也是不允許的。要想深刻理解這一概念,第一次看可能會(huì)有點(diǎn)繞,建議有條件的情況下多進(jìn)行一些實(shí)驗(yàn)。
剛才提到了方法參數(shù)上協(xié)變和反變的相互影響。那么方法的返回值會(huì)不會(huì)有同樣的問(wèn)題呢?我們看如下代碼:
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();
}
我們看到和剛剛正好相反,如果一個(gè)接口需要對(duì)T進(jìn)行協(xié)變或反變,那么這個(gè)接口所有方法的返回值類(lèi)型必須支持對(duì)T同樣方向的協(xié)變或反變。這就是方法返回值的協(xié)變-反變一致原則。也就是說(shuō),即使in參數(shù)也可以用于方法的返回值類(lèi)型,只要借助一個(gè)可以反變的類(lèi)型作為橋梁即可。如果對(duì)這個(gè)過(guò)程還不是特別清楚,建議也是寫(xiě)一些代碼來(lái)進(jìn)行實(shí)驗(yàn)。至此我們發(fā)現(xiàn)協(xié)變和反變有許多有趣的特性,以至于在代碼里in和out都不像他們字面意思那么好理解。當(dāng)你看到in參數(shù)出現(xiàn)在返回值類(lèi)型,out參數(shù)出現(xiàn)在參數(shù)類(lèi)型時(shí),千萬(wàn)別暈倒,用本文的知識(shí)即可破解其中奧妙。
總結(jié)
經(jīng)過(guò)本文的講解,大家應(yīng)該已經(jīng)初步了解的協(xié)變和反變的含義,能夠分清協(xié)變、反變的過(guò)程。我們還討論了.NET 4.0支持泛型接口、委托的協(xié)變和反變的新功能和新語(yǔ)法。最后我們還套了論的協(xié)變、反變與函數(shù)參數(shù)、返回值的相互作用原理,以及由此產(chǎn)生的奇妙寫(xiě)法。我希望大家看了我的文章后,能夠?qū)⑦@些知識(shí)用于泛型程序設(shè)計(jì)當(dāng)中,正確運(yùn)用.NET 4.0的新增功能。祝大家使用愉快!
相關(guān)文章
C#實(shí)現(xiàn)BBcode轉(zhuǎn)為Markdown的方法
這篇文章主要給大家介紹了關(guān)于C#實(shí)現(xiàn)BBcode轉(zhuǎn)Markdown的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧。2018-02-02
深入了解c# 設(shè)計(jì)模式之簡(jiǎn)單工廠模式
這篇文章主要介紹了c# 設(shè)計(jì)模式之簡(jiǎn)單工廠模式的的相關(guān)資料,文中代碼非常詳細(xì),幫助大家更好的理解和學(xué)習(xí),感興趣的朋友可以了解下2020-06-06
C#實(shí)現(xiàn)異步日志記錄類(lèi)的示例代碼
這篇文章主要為大家詳細(xì)介紹了C#如何實(shí)現(xiàn)異步日志記錄類(lèi),從而方便下次使用,不用重復(fù)造輪子,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以了解下2023-11-11
C#實(shí)現(xiàn)身份證驗(yàn)證功能的示例代碼
這篇文章主要為大家詳細(xì)介紹了如何利用C#實(shí)現(xiàn)身份證驗(yàn)證功能,文中的示例代碼講解詳細(xì),對(duì)我們學(xué)習(xí)C#有一定的幫助,感興趣的小伙伴可以跟隨小編一起了解一下2022-12-12
Unity實(shí)現(xiàn)文本轉(zhuǎn)貼圖
這篇文章主要為大家詳細(xì)介紹了Unity實(shí)現(xiàn)文本轉(zhuǎn)貼圖,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-05-05

