簡單解釋共變數與反變數 Covariance and contravariance in C#

因為很容易忘記,然後每次都要再查一遍,所以決定寫一篇。

說明

Covariance 共變數, 代表可以在原本的型別處使用衍伸型別(使用繼承後的子類)
Contravariance 反變數, 代表可以在原本的型別處使用更泛化的型別(使用自身的父類)

問題

在一個支援多型的語言中,偶爾會遇到下面幾個狀況,需要語言設計者定義

  • 可以指派子類給父型別嗎?
  • 在泛型的狀況中,可以指派子類的泛型給父類的泛型變數嗎?
  • 在泛型委託(generic delegate)的狀況中,能夠使用同樣繼承樹的型別嗎?

思考

什麼時候我們可以安全地使用子類呢?

  • 如果我們只是把子類當成父類用,那很ok
  • 如果我們把子類丟進父類參數的方法做運算, 似乎也很ok
  • 如果我們回傳子類, 但回傳值的型別是父類, 似乎也很ok
  • 但有一個情況不太ok

    看起來操作是合法的,但是如果底層的物件是List<Dog>, 那加入Cat是不是個合法的行為,就有待商榷。

語言規範

語言的設計者必須要回答上述問題。在C# 4.0之後,語言的回答是:

  • 如果你沒有對泛型參數做任何約束,那麼會採用最嚴格的不變性invariance.
  • 但如果有針對泛型的參數作約束(in/out),那編譯器就允許你做你想做的事。

    out

    如果你去看MSDN關於IEnumerable<T> ,會看到T前面有一個out

    這個 out 的意思是,我們對這個T多做一些約束,對於所有該介面的方法,T只能用在回傳值

    一旦我們限定T只能出現在回傳值,代表所有interface<Child>都可以正常被當成interface<Parent>來使用

    in

    in 是另外一種泛型約束,強調該T只能用在介面的參數,不能用在回傳值

    這代表該使用 IComparer<Child> 的位置, 能夠接受傳入更一般化 IComparer<Parent>

    無約束

    而如果沒有做泛型in, out的約束, 可能就會導致型別不安全的結果。

    在C#中,有一個因為歷史因素導致的Covariance,那就是array, 支援Covariance, 代表可以在原本的型別處使用衍伸型別(使用繼承後的子類)。設計該行為時,泛型還沒出現。

    泛型委派(generic delegate)

    泛型委派其實可以視為一種極為簡單的介面。(使用時常常被拿來傳入匿名方法,可以想成是實作同樣簽章的不同實體)

    常見的有 Func<T,TResult>, Action<T1,T2>, Predicate<T>

    在使用Func<in T,out TResult>泛型委派的地方

目前.NET中常見的使用Covariance and contravariance的類別

  • 直接參閱 MSDN 最下面
    通常都是些IEnumerable, IGrouping, IQueryable, IComparable, Action, Func這些
  • C# 9之後才支援覆寫方法回傳值的共變性

END

希望這邊文章足夠深入淺出, 幫助到想了解共變數反變數的大家

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *