理解 C# 泛型接口中的協變與逆變(抗變)

最近在看《C#高級編程(第九版)》這本書,看到了泛型接口這章。其中關於協變和逆變沒太理解,講得有點坑爹,網上查了許多資料,總算(感受)弄清楚了,來這裏記錄一下。編程

1、協變和逆變是什麼?

先從字面上理解 協變(Covariance)逆變(Contravariance)函數

co- 是英文中表示「協同」、「合做」的前綴。協變 的字面意思就是 「與變化的方向相同」
contra- 是英文中表示「相反」的前綴,逆變 的字面意思就是是 「與變化方向相反」code

那麼問題來了,挖掘機技術哪家強?這裏的 變化方向 指的是什麼?對象

C# 中對於對象(即對象引用),僅存在一種隱式類型轉換,即 子類型的對象引用到父類型的對象引用的轉換。這裏的變化指的就是這種 子->父 的類型轉換。接口

object o = "hello";
//string (子類)類型的引用轉換爲 object (父類)類型的引用

協變逆變雖然從名字上看是兩個徹底相反的轉換,但其實只是「子類型引用到父類型引用」這一過程在函數中使用的 兩個不一樣階段 而已,接下來將詳細說明這點。string

2、使用函數的不一樣階段發生的類型轉換

假設有一函數,接收 object 類型的參數,輸出 string 類型的返回值:class

string Method(object o)
{
    return "abc";
}

那麼在Main函數中咱們能夠這樣調用它:泛型

string s = "abc";
object o = Method(s);

注意,這裏發生了兩次隱式類型轉換:object

  1. 在向函數輸入時,參數 sstring 類型轉換爲 object 類型引用

  2. 在函數輸出(返回)時,返回值 由 string 類型轉換爲 object 類型

咱們這裏能夠看做是函數簽名可發生變換(不論函數的內容,不影響結果):

  1. string Method(object o) 可變換爲 string Method(string o)

  2. string Method(string o) 可變換爲 object Method(string o)

也就是說,在函數輸入時,函數的 輸入類型 可由 object 變換爲 string父->子
在函數輸出時,函數的 輸出類型 可由string變換爲object子->父

3、理解泛型接口中的 inout參數

沒有指定inout的狀況

假設有一泛型接口,而且有一個類實現了此接口:

interface IDemo<T>
{
    T Method(T value);
}
public class Demo : IDemo<string>
{
    //實現接口 IDemo<string>

    public string Method(string value)
    {
        return value;
    }
}

Main函數中這樣寫:

IDemo<string> demoStr = new Demo();
IDemo<object> demoObj = demoStr;

上面的這段代碼中的第二行包含了一個假設:

IDemo<string> 類型可以隱式轉換爲 IDemo<object> 類型

這乍看上去就像「子類型引用轉換爲父類型引用」 同樣,然而很遺憾,他們並不相同。假如能夠進行隱式類型轉換,那就意味着:

string Method(string value) 能轉換爲 object Method(object value)

從上一節中咱們知道,在函數這輸入和輸出階段,其類型可變化方向是不一樣的。
因此在C#中,要想應用泛型接口類型的隱式轉換,須要討論「輸入」和「輸出」兩種狀況。

接口僅用於輸出的狀況,協變

interface IDemo<out T>
{
    //僅將類型 T 用於輸出
    T Method(object value);
}

public class Demo : IDemo<string>
{
    //實現接口
    public string Method (object value)
    {
        //別忘了類型轉換!
        return value.ToString();
    }
}

Main函數中這樣寫:

IDemo<string> demoStr = new Demo();
IDemo<object> demoObj = demoStr;

可將 string Method (object value) 轉換爲 object Method (object value)
便可將 IDemo<string> 類型轉換爲 IDemo<object> 類型。
僅從泛型的類型上看,這是 「子->父」 的轉換,與第一節中提到的轉換方向相同,稱之爲「協變」

接口僅用於輸入的狀況,逆變

同理咱們能夠給 T 加上 in 參數:

interface IDemo<in T>
{
    //僅將類型 T 用於輸入
    string Method(T value);
}

public class Demo : IDemo<object>
{
    //實現接口
    public string Method (object value)
    {
        return value.ToString();
    }
}

Main函數中這樣寫:

IDemo<object> demoObj = new Demo();
IDemo<string> demoStr = demoObj;

這裏可將 string Method (object value) 轉換爲 string Method (string value)
便可將 IDemo<object> 類型轉換爲 IDemo<string> 類型。
僅從泛型的類型上看,這是 「父->子」 的轉換,與第一節中提到的轉換方向相反,稱之爲「逆變」,有時也譯做「抗變」或「反變」。

4、總結

以上只討論了協變與逆變在方法中的狀況,其實在屬性中狀況也相相似,再也不說明。

可能你們也發現了,所謂「協」與「逆」都是隻是一種表象,其內在本質爲同一過程。

「協變」與「逆變」中的「協」與「逆」表示泛型接口在將類型參數僅用於輸入或輸出的狀況下,其類型參數的隱式轉換所遵循的規律。

協變

當泛型接口類型僅用於輸出(使用關鍵詞 out),其類型參數隱式轉換所遵循的規律與對象引用的類型轉換規律相同,稱之爲「協變」

逆變

當泛型接口類型僅用於輸入(使用關鍵詞 in),其類型參數隱式轉換所遵循的規律與對象引用的類型轉換規律相反,稱之爲「逆變」、「抗變」或「反變」。


=。= 感受強類型語言有點坑爹啊

相關文章
相關標籤/搜索