1. 程式人生 > 實用技巧 >2020.12.02

2020.12.02

技術標籤:# 逆變與協變

逆變(contravariant)與協變(covariant)是C#4新增的概念,許多書籍和部落格都有講解,我覺得都沒有把它們講清楚,搞明白了它們,可以更準確地去定義泛型委託和介面,這裡我嘗試畫圖詳細解析逆變與協變。

變的概念

我們都知道.Net裡或者說在OO的世界裡,可以安全地把子類的引用賦給父類引用,例如:

//父類 = 子類
string str = "string";
object obj = str;//變了

C#裡又有泛型的概念,泛型是對型別系統的進一步抽象,比上面簡單的型別高階,把上面的變化體現在泛型的引數上就是我們所說的逆變與協變的概念。通過在泛型引數上使用in或out關鍵字,可以得到逆變或協變的能力。下面是一些對比的例子:

協變(Foo<父類> = Foo<子類>):

//泛型委託:
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<父類>

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泛型引數用作方法返回值的泛型介面時:

public interface IPlayB<in T>
{
    T Test();
}

出現瞭如下編譯錯誤:

錯誤    1    方差無效: 型別引數“T”必須為“CovarianceAndContravariance.IPlayB<T>.Test()”上有效的 協變式。“T”為 逆變。  

到這裡,我們大致知道了逆變與協變的相關概念,那麼為什麼把泛型引數限制為in或者out就可以“變”呢?下面嘗試畫圖解釋原理。

協變不是理所當然的,逆變也沒有“逆”

我們先來看看不支援逆變與協變的泛型,把子類賦給父類,再執行父類方法的具體流程,對於這樣一個簡單的例子的Test方法:

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的泛型引數來說,肯定 是行不通的,其中一個方向必然不能安全轉換的。例如,對上面的例子,我們嘗試“變”:

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

這裡的“實際流程”如下,可以看到,引數那裡是object是不能安全轉換為string,所以編譯失敗:
image

看到這裡如果都明白的話,我們不難得到逆變與協變的”實際流程圖”(記住,它們是有in/out限制的):

image

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

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

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

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

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!