Skip to main content

簡單解釋共變數(covariance)與反變數(contravariance)

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

說明

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

問題

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

  • 可以指派子類給父型別嗎?

    Fruit fruit = new Apple()
  • 在泛型的狀況中,可以指派子類的泛型給父類的泛型變數嗎?

    // 可以指派List嗎?
    List<Fruit> fruits = new List<Apple>();

    // 可以指派陣列嗎?
    Fruit[] fruits = new Apple[];

    // 可以指派IEnumerable<T>嗎?
    IEnumerable<Fruit> fruits = new IEnumerable<Apple>();
  • 在泛型委託(generic delegate)的狀況中,能夠使用同樣繼承樹的型別嗎?

    public class Type1 {}
    public class Type2 : Type1 {}
    public class Type3 : Type2 {}

    public static Type3 MyMethod(Type1 t)
    {
    // ...
    }

    Func<Type2, Type2> f1 = MyMethod;
    Func<Type3, Type1> f2 = f1;
    Type1 t1 = f2(new Type3());

思考

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

  • 如果我們只是把子類當成父類用,那很ok
Animal animal = new Dog()
animal.Move();
  • 如果我們把子類丟進父類參數的方法做運算, 似乎也很ok
public int Sell(Animal animal) {
return animal.GetPrice();
}
Sell(new Dog());
  • 如果我們回傳子類, 但回傳值的型別是父類, 似乎也很ok
public Animal Buy() {
return new Dog();
}
Sell(new Dog());
  • 但有一個情況不太ok
List<Animal> animals = new List<Dog>() {dog1, dog2};
animals.Add(new Cat()); // 這樣真的可以嗎?

看起來操作是合法的,但是如果底層的物件是List<Dog>, 那又怎麼有辦法加入Cat呢?

語言規範

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

  • 如果你沒有對泛型參數做任何約束,那麼會採用最嚴格的不變性invariance.

    List<Animal> animals = new List<Dog>() {dog1, dog2};
    animals.Add(new Cat()); // 這樣不行
    // Error CS0029 Cannot implicitly convert type
    // 'System.Collections.Generic.List<Test.Dog>' to
    // 'System.Collections.Generic.List<Test.Animal>'
  • 但如果有針對泛型的參數作約束(in/out),那編譯器就允許你做你想做的事。

    IEnumerable<Animal> animals = new List<Dog>(); // Fine

    out

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

    public interface IEnumerable<out T> : System.Collections.IEnumerable

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

    interface IEnumerable<out T>
    {
    public IEnumerator<T> GetEnumerator ();
    }
    public System.Collections.Generic.IEnumerator<out T> GetEnumerator ();
    // IEnumerable<T>只有GetEnumerator這個方法,而且該方法的T是放在回傳值而不是參數

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

    interface Querable<out T> {
    T Query()
    }
    Query<ParentDto> query = new Query<ChildDto>(); // 實作可能是子類, 可能是父類
    ParentDto parent = query.Query(); // 但使用該介面的人都當成父類用

    in

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

    public interface IComparer<in T> {
    public int Compare (T? x, T? y);
    }

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

    public SortedSet<T>(IComparer<T>)  // 這是SortedSet的constructor, 支援傳入一個IComparer

    new SortedSet<Circle>(new Comparer<Circle>()); // 這是常見的情況
    new SortedSet<Circle>(new Comparer<Shape>());
    // 這也沒有問題, 因為在constructor內使用IComparer的地方,
    // 一定會傳入Circle給Compare, 而且Circle在介面上只用於傳入值
    // 因此一定可以被當成Shape, 所以功能正常沒有問題

    無約束

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

    public class List<T> // 沒有任何in, out的約束
    List<Animal> animals = new List<Dog>() {dog1, dog2}; // GG, compile error

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

    // compile pass
    string[] strings = new string[1];
    object[] objects = strings;
    objects[0] = new object(); // GG, will throw exception at run time

    泛型委派(generic delegate)

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

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

    // 舉Func為例, 參數部分都加了in, 回傳值的部分加了out
    public delegate TResult Func<in T,out TResult>(T arg);
    public delegate void Action<in T1,in T2,in T3>(T1 arg1, T2 arg2, T3 arg3);
    public delegate bool Predicate<in T>(T obj);

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


    Source> Filter(Func<TSource,bool> predicate);

    //傳入參數更為寬鬆, 回傳值更為嚴謹的delegate, 是沒有問題的
    {
    ...
    var filteredResult = Filter(Predicate);
    ...
    }
    bool Predicate(Parent) {
    return Parent
    }

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

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

END

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