[摘要]逆變(contravariant)與協(xié)變(covariant)是C#4新增的概念,許多書(shū)籍和博客都有講解,我覺(jué)得都沒(méi)有把它們講清楚,搞明白了它們,可以更準(zhǔn)確地去定義泛型委托和接口,這里我嘗試畫(huà)圖詳細(xì)解析逆變與協(xié)變。
逆變(contravariant)與協(xié)變(covariant)是C#4新增的概念,許多書(shū)籍和博客都有講解,我覺(jué)得都沒(méi)有把它們講清楚,搞明白了它們,可以更準(zhǔn)確地去定義泛型委托和接口,這里我嘗試畫(huà)圖詳細(xì)解析逆變與協(xié)變。
變的概念
我們都知道.Net里或者說(shuō)在OO的世界里,可以安全地把子類的引用賦給父類引用,例如:
View Row Code1//父類 = 子類
2string str = "string";
3object obj = str;//變了
而C#里又有泛型的概念,泛型是對(duì)類型系統(tǒng)的進(jìn)一步抽象,比上面簡(jiǎn)單的類型高級(jí),把上面的變化體現(xiàn)在泛型的參數(shù)上就是我們所說(shuō)的逆變與協(xié)變的概念。通過(guò)在泛型參數(shù)上使用in或out關(guān)鍵字,可以得到逆變或協(xié)變的能力。下面是一些對(duì)比的例子:
協(xié)變(Foo<父類> = Foo<子類> ):
View Row Code1//泛型委托:
2
3public delegate T MyFuncA<T>();//不支持逆變與協(xié)變
4
5public delegate T MyFuncB<out T>();//支持協(xié)變
6
7
8
9MyFuncA<object> funcAObject = null;
10
11MyFuncA<string> funcAString = null;
12
13MyFuncB<object> funcBObject = null;
14
15MyFuncB<string> funcBString = null;
16
17MyFuncB<int> funcBInt = null;
18
19
20
21funcAObject = funcAString;//編譯失敗,MyFuncA不支持逆變與協(xié)變
22
23funcBObject = funcBString;//變了,協(xié)變
24
25funcBObject = funcBInt;//編譯失敗,值類型不參與協(xié)變或逆變
26
27
28
29//泛型接口
30
31public interface IFlyA<T> { }//不支持逆變與協(xié)變
32
33public interface IFlyB<out T> { }//支持協(xié)變
34
35
36
37IFlyA<object> flyAObject = null;
38
39IFlyA<string> flyAString = null;
40
41IFlyB<object> flyBObject = null;
42
43IFlyB<string> flyBString = null;
44
45IFlyB<int> flyBInt = null;
46
47
48
49flyAObject = flyAString;//編譯失敗,IFlyA不支持逆變與協(xié)變
50
51flyBObject = flyBString;//變了,協(xié)變
52
53flyBObject = flyBInt;//編譯失敗,值類型不參與協(xié)變或逆變
54
55
56
57//數(shù)組:
58
59string[] strings = new string[] { "string" };
60
61object[] objects = strings;
逆變(Foo<子類> = Foo<父類>)
View Row Codepublic delegate void MyActionA<T>(T param);//不支持逆變與協(xié)變public delegate void MyActionB<in T>(T param);//支持逆變 public interface IPlayA<T> { }//不支持逆變與協(xié)變public interface IPlayB<in T> { }//支持逆變 MyActionA<object> actionAObject = null;MyActionA<string> actionAString = null;MyActionB<object> actionBObject = null;MyActionB<string> actionBString = null;actionAString = actionAObject;//MyActionA不支持逆變與協(xié)變,編譯失敗actionBString = actionBObject;//變了,逆變 IPlayA<object> playAObject = null;IPlayA<string> playAString = null;IPlayB<object> playBObject = null;IPlayB<string> playBString = null;playAString = playAObject;//IPlayA不支持逆變與協(xié)變,編譯失敗playBString = playBObject;//變了,逆變
來(lái)到這里我們看到有的能變,有的不能變,要知道以下幾點(diǎn):
·以前的泛型系統(tǒng)(或者說(shuō)沒(méi)有in/out關(guān)鍵字時(shí)),是不能“變”的,無(wú)論是“逆”還是“順(協(xié))”。
·當(dāng)前僅支持接口和委托的逆變與協(xié)變 ,不支持類和方法。但數(shù)組也有協(xié)變性。
·值類型不參與逆變與協(xié)變。
那么in/out是什么意思呢?為什么加了它們就有了“變”的能力,是不是我們定義泛型委托或者接口都應(yīng)該添加它們呢?
原來(lái),在泛型參數(shù)上添加了in關(guān)鍵字作為泛型修飾符的話,那么那個(gè)泛型參數(shù)就只能用作方法的輸入?yún)?shù),或者只寫屬性的參數(shù),不能作為方法返回值等,總之就是只能是“入”,不能出。out關(guān)鍵字反之。
當(dāng)嘗試編譯下面這個(gè)把in泛型參數(shù)用作方法返回值的泛型接口時(shí):
View Row Code1public interface IPlayB<in T>
2
3{
4
5 T Test();
6
7}
出現(xiàn)了如下編譯錯(cuò)誤:
錯(cuò)誤 1 方差無(wú)效: 類型參數(shù)“T”必須為“CovarianceAndContravariance.IPlayB<T>.Test()”上有效的 協(xié)變式?!癟”為 逆變。 \
這里,我們大致知道了逆變與協(xié)變的相關(guān)概念,那么為什么把泛型參數(shù)限制為in或者out就可以“變”呢?下面嘗試畫(huà)圖解釋原理。
協(xié)變不是理所當(dāng)然的,逆變也沒(méi)有“逆”
我們先來(lái)看看不支持逆變與協(xié)變的泛型,把子類賦給父類,再執(zhí)行父類方法的具體流程,對(duì)于這樣一個(gè)簡(jiǎn)單的例子的Test方法:
View Row Code1public interface Base<T>
2
3{
4
5 T Test(T param);
6
7}
8
9public class Sub<T> : Base<T>
10
11{
12
13 public T Test(T param) { return default(T); }
14
15}
16
17Base<string> b = new Sub<string>();
18
19b.Test("");
它實(shí)際的流程是這樣的:
即調(diào)用父類的方法,其實(shí)實(shí)際是調(diào)用子類的方法。可以看到,這個(gè)方法能夠安全的調(diào)用,需要兩個(gè)條件:1.變式(父)的方法參數(shù)能安全轉(zhuǎn)為原式(子)的參數(shù);2.原式(子)的返回值能安全的轉(zhuǎn)為變式的返回值。不幸的是參數(shù)的流向跟返回值的流向是相反的,所以對(duì)于既是in,又是out的泛型參數(shù)來(lái)說(shuō),肯定是行不通的,其中一個(gè)方向必然不能安全轉(zhuǎn)換的。例如,對(duì)上面的例子,我們嘗試“變”:
View Row Code1Base<object> BaseObject = null;
2
3Base<string> BaseString = null;
4
5BaseObject = BaseString;//編譯失敗
6
7BaseObject.Test("");
這里的“實(shí)際流程”如下,可以看到,參數(shù)那里是object是不能安全轉(zhuǎn)換為string,所以編譯失?。?div style="height:15px;">
來(lái)到這里應(yīng)該基本理解逆變與協(xié)變了,不過(guò)裝配腦袋的這篇文章有個(gè)更高級(jí)的問(wèn)題,原文也有解答,這里我用上面畫(huà)圖的方式去理解它。
答案是,如果是in的話,會(huì)編譯失敗,out才正確(當(dāng)然不要泛型修飾符也能通過(guò)編譯,但I(xiàn)Foo就沒(méi)有協(xié)變能力了)。這里的意思就是說(shuō),一個(gè)有協(xié)變(逆變)能力的泛型(IBar),作為另一個(gè)泛型(IFoo)的參數(shù)時(shí),影響到了它(IFoo)的泛型的定義。乍一看以為是in的其中一個(gè)陷阱是T是在Test方法的參數(shù)里的,所以以為是in。但這里Test的參數(shù)根本不是T,而是IBar<T>。
圖跟前面那些大致一樣,但理解它要跟問(wèn)題相反(上面問(wèn)題是先定義好IBar,再去定義IFoo)。1.我們定義好一個(gè)有協(xié)變能力的IFoo,這是前提。2.可以推出,上面的流程是成立的。3.這個(gè)流程重點(diǎn)是參數(shù)流向,要使整個(gè)流程成立,就必須使IBar<string> = IBar<object>成立,這不就是逆變嗎?整個(gè)結(jié)論就是,有協(xié)變能力的IFoo要求它的泛型參數(shù)(IBar)有逆變能力。其實(shí)根據(jù)上面的箭頭也可以理解,因?yàn)樵胶妥兪降淖兿蚋鷧?shù)的變向是相反的,導(dǎo)致了它們要有相反的能力,這就是裝配腦袋文章說(shuō)的:方法參數(shù)的協(xié)變-反變互換原則。根據(jù)這個(gè)原理,也很容易得出,如果Test方法的返回值是IBar<T>,而不是參數(shù),那么就要求IBar<T>要有協(xié)變能力,因?yàn)榉祷刂档募^與原式和變式的變向的箭頭是同向的。