逆變(contravariant)與協變(covariant)是C#4新增的概念,許多書籍和博客都有講解,我覺得都沒有把它們講清楚,搞明白了它們,可以更准確地去定義泛型委托和接口,這里我嘗試畫圖詳細解析逆變與協變。
變的概念
我們都知道.Net里或者說在OO的世界里,可以安全地把子類的引用賦給父類引用,例如:
1
2
3
|
//父類 = 子類
string
str =
"string"
;
object
obj = str;
//變了
|
而C#里又有泛型的概念,泛型是對類型系統的進一步抽象,比上面簡單的類型高級,把上面的變化體現在泛型的參數上就是我們所說的逆變與協變的概念。通過在泛型參數上使用in或out關鍵字,可以得到逆變或協變的能力。下面是一些對比的例子:
協變(Foo<父類> = Foo<子類> ):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
//泛型委托:
public
delegate
T MyFuncA<T>();
//不支持逆變與協變
public
delegate
T MyFuncB<
out
T>();
//支持協變
MyFuncA<
object
> funcAObject =
null
;
MyFuncA<
string
> funcAString =
null
;
MyFuncB<
object
> funcBObject =
null
;
MyFuncB<
string
> funcBString =
null
;
MyFuncB<
int
> funcBInt =
null
;
funcAObject = funcAString;
//編譯失敗,MyFuncA不支持逆變與協變
funcBObject = funcBString;
//變了,協變
funcBObject = funcBInt;
//編譯失敗,值類型不參與協變或逆變
//泛型接口
public
interface
IFlyA<T> { }
//不支持逆變與協變
public
interface
IFlyB<
out
T> { }
//支持協變
IFlyA<
object
> flyAObject =
null
;
IFlyA<
string
> flyAString =
null
;
IFlyB<
object
> flyBObject =
null
;
IFlyB<
string
> flyBString =
null
;
IFlyB<
int
> flyBInt =
null
;
flyAObject = flyAString;
//編譯失敗,IFlyA不支持逆變與協變
flyBObject = flyBString;
//變了,協變
flyBObject = flyBInt;
//編譯失敗,值類型不參與協變或逆變
//數組:
string
[] strings =
new
string
[] {
"string"
};
object
[] objects = strings;
|
逆變(Foo<子類> = Foo<父類>)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
public
delegate
void
MyActionA<T>(T param);
//不支持逆變與協變
public
delegate
void
MyActionB<
in
T>(T param);
//支持逆變
public
interface
IPlayA<T> { }
//不支持逆變與協變
public
interface
IPlayB<
in
T> { }
//支持逆變
MyActionA<
object
> actionAObject =
null
;
MyActionA<
string
> actionAString =
null
;
MyActionB<
object
> actionBObject =
null
;
MyActionB<
string
> actionBString =
null
;
actionAString = actionAObject;
//MyActionA不支持逆變與協變,編譯失敗
actionBString = actionBObject;
//變了,逆變
IPlayA<
object
> playAObject =
null
;
IPlayA<
string
> playAString =
null
;
IPlayB<
object
> playBObject =
null
;
IPlayB<
string
> playBString =
null
;
playAString = playAObject;
//IPlayA不支持逆變與協變,編譯失敗
playBString = playBObject;
//變了,逆變
|
來到這里我們看到有的能變,有的不能變,要知道以下幾點:
- 以前的泛型系統(或者說沒有in/out關鍵字時),是不能“變”的,無論是“逆”還是“順(協)”。
- 當前僅支持接口和委托的逆變與協變 ,不支持類和方法。但數組也有協變性。
- 值類型不參與逆變與協變。
那么in/out是什么意思呢?為什么加了它們就有了“變”的能力,是不是我們定義泛型委托或者接口都應該添加它們呢?
原來,在泛型參數上添加了in關鍵字作為泛型修飾符的話,那么那個泛型參數就只能用作方法的輸入參數,或者只寫屬性的參數,不能作為方法返回值等,總之就是只能是“入”,不能出。out關鍵字反之。
當嘗試編譯下面這個把in泛型參數用作方法返回值的泛型接口時:
1
2
3
4
|
public
interface
IPlayB<
in
T>
{
T Test();
}
|
出現了如下編譯錯誤:
錯誤 1 方差無效: 類型參數“T”必須為“CovarianceAndContravariance.IPlayB<T>.Test()”上有效的 協變式。“T”為 逆變。
到這里,我們大致知道了逆變與協變的相關概念,那么為什么把泛型參數限制為in或者out就可以“變”呢?下面嘗試畫圖解釋原理。
協變不是理所當然的,逆變也沒有“逆”
我們先來看看不支持逆變與協變的泛型,把子類賦給父類,再執行父類方法的具體流程,對於這樣一個簡單的例子的Test方法:
1
2
3
4
5
6
7
8
9
10
|
public
interface
Base<T>
{
T Test(T param);
}
public
class
Sub<T> : Base<T>
{
public
T Test(T param) {
return
default
(T); }
}
Base<
string
> b =
new
Sub<
string
>();
b.Test(
""
);
|
即調用父類的方法,其實實際是調用子類的方法。可以看到,這個方法能夠安全的調用,需要兩個條件:1.變式(父)的方法參數能安全轉為原式(子)的 參數;2.原式(子)的返回值能安全的轉為變式的返回值。不幸的是參數的流向跟返回值的流向是相反的,所以對於既是in,又是out的泛型參數來說,肯定 是行不通的,其中一個方向必然不能安全轉換的。例如,對上面的例子,我們嘗試“變”:
1
2
3
4
|
Base<
object
> BaseObject =
null
;
Base<
string
> BaseString =
null
;
BaseObject = BaseString;
//編譯失敗
BaseObject.Test(
""
);
|
這里的“實際流程”如下,可以看到,參數那里是object是不能安全轉換為string,所以編譯失敗:
看到這里如果都明白的話,我們不難得到逆變與協變的”實際流程圖”(記住,它們是有in/out限制的):
可以看到,從”實際流程圖”來看,逆變根本沒有“逆”,都離不開只能安全地把子類的引用賦給父類引用這個根本。
來到這里應該基本理解逆變與協變了,不過裝配腦袋的這篇文章有個更高級的問題,原文也有解答,這里我用上面畫圖的方式去理解它。
圖解逆變與協變的相互作用
問題的提出,你知道那個正確嗎?
1
2
3
4
5
6
7
8
9
10
11
|
public
interface
IBar<
in
T> { }
//應該是in
public
interface
IFoo<
in
T>
{
void
Test(IBar<T> bar);
}
//還是out
public
interface
IFoo<
out
T>
{
void
Test(IBar<T> bar);
}
|
答案是,如果是in的話,會編譯失敗,out才正確(當然不要泛型修飾符也能通過編譯,但IFoo就沒有協變能力了)。這里的意思就是說,一個有協 變(逆變)能力的泛型(IBar),作為另一個泛型(IFoo)的參數時,影響到了它(IFoo)的泛型的定義。乍一看以為是in的其中一個陷阱是T是在 Test方法的參數里的,所以以為是in。但這里Test的參數根本不是T,而是IBar<T>。
我們畫個圖來理解它。既然out可以通過,那么它的“協變流程圖”應該如下:
圖跟前面那些大致一樣,但理解它要跟問題相反(上面問題是先定義好IBar,再去定義IFoo)。1.我們定義好一個有協變能力的IFoo,這是前 提。2.可以推出,上面的流程是成立的。3.這個流程重點是參數流向,要使整個流程成立,就必須使IBar<string> = IBar<object>成立,這不就是逆變嗎?整個結論就是,有協變能力的IFoo要求它的泛型參數(IBar)有逆變能力。其實根據上面的箭頭也可以理解,因為原式和變式的變向跟參數的變向是相反的,導致了它們要有相反的能力,這就是裝配腦袋文章說的:方法參數的協變-反變互換原則。根據這個原理,也很容易得出,如果Test方法的返回值是IBar<T>,而不是參數,那么就要求IBar<T>要有協變能力,因為返回值的箭頭與原式和變式的變向的箭頭是同向的。
The End!
轉自:http://www.cnblogs.com/lemontea/archive/2013/02/17/2915065.html