逆變與協變詳解

逆變(contravariant)與協變(covariant)是C#4新增的概念,許多書籍和博客都有講解,我以爲都沒有把它們講清楚,搞明白了它們,能夠更準確地去定義泛型委託和接口,這裏我嘗試畫圖詳細解析逆變與協變html

變的概念

咱們都知道.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; //變了,逆變

來到這裏咱們看到有的能變,有的不能變,要知道如下幾點:spa

  • 之前的泛型系統(或者說沒有in/out關鍵字時),是不能「變」的,不管是「逆」仍是「順(協)」。
  • 當前僅支持接口和委託的逆變與協變 ,不支持類和方法。但數組也有協變性。
  • 值類型不參與逆變與協變。

那麼in/out是什麼意思呢?爲何加了它們就有了「變」的能力,是否是咱們定義泛型委託或者接口都應該添加它們呢?code

原來,在泛型參數上添加了in關鍵字做爲泛型修飾符的話,那麼那個泛型參數就只能用做方法的輸入參數,或者只寫屬性的參數,不能做爲方法返回值等,總之就是隻能是「入」,不能出。out關鍵字反之。htm

當嘗試編譯下面這個把in泛型參數用做方法返回值的泛型接口時:blog

?
1
2
3
4
public interface IPlayB< in T>
{
     T Test();
}

出現了以下編譯錯誤:接口

錯誤    1    方差無效: 類型參數「T」必須爲「CovarianceAndContravariance.IPlayB<T>.Test()」上有效的 協變式。「T」爲 逆變。  ci

到這裏,咱們大體知道了逆變與協變的相關概念,那麼爲何把泛型參數限制爲in或者out就能夠「變」呢?下面嘗試畫圖解釋原理。get

協變不是理所固然的,逆變也沒有「逆」

咱們先來看看不支持逆變與協變的泛型,把子類賦給父類,再執行父類方法的具體流程,對於這樣一個簡單的例子的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( "" );

它實際的流程是這樣的:

image

即調用父類的方法,其實實際是調用子類的方法。能夠看到,這個方法可以安全的調用,須要兩個條件:1.變式(父)的方法參數能安全轉爲原式(子)的 參數;2.原式(子)的返回值能安全的轉爲變式的返回值。不幸的是參數的流向跟返回值的流向是相反的,因此對於既是in,又是out的泛型參數來講,確定 是行不通的,其中一個方向必然不能安全轉換的。例如,對上面的例子,咱們嘗試「變」:

?
1
2
3
4
Base< object > BaseObject = null ;
Base< string > BaseString = null ;
BaseObject = BaseString; //編譯失敗
BaseObject.Test( "" );

這裏的「實際流程」以下,能夠看到,參數那裏是object是不能安全轉換爲string,因此編譯失敗:

image

看到這裏若是都明白的話,咱們不可貴到逆變與協變的」實際流程圖」(記住,它們是有in/out限制的):

image

能夠看到,從」實際流程圖」來看,逆變根本沒有「逆」,都離不開只能安全地把子類的引用賦給父類引用這個根本。

來到這裏應該基本理解逆變與協變了,不過裝配腦殼的這篇文章有個更高級的問題,原文也有解答,這裏我用上面畫圖的方式去理解它。

圖解逆變與協變的相互做用

問題的提出,你知道那個正確嗎?

?
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能夠經過,那麼它的「協變流程圖」應該以下:

image

圖跟前面那些大體同樣,但理解它要跟問題相反(上面問題是先定義好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

相關文章
相關標籤/搜索