因為很容易忘記,然後每次都要再查一遍,所以決定寫一篇。
說明
Covariance 共變數, 代表可以在原本的型別處使用衍伸型別(使用繼承後的子類)
Contravariance 反變數, 代表可以在原本的型別處使用更泛化的型別(使用自身的父類)
問題
在一個支援多型的語言中,偶爾會遇到下面幾個狀況,需要語言設計者定義
- 可以指派子類給父型別嗎?
1Fruit fruit = new Apple() - 在泛型的狀況中,可以指派子類的泛型給父類的泛型變數嗎?
12345678// 可以指派List嗎?List<Fruit> fruits = new List<Apple>();// 可以指派陣列嗎?Fruit[] fruits = new Apple[];// 可以指派IEnumerable<T>嗎?IEnumerable<Fruit> fruits = new List<Apple>(); - 在泛型委託(generic delegate)的狀況中,能夠使用同樣繼承樹的型別嗎?
123456789101112public 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
12Animal animal = new Dog()animal.Move(); - 如果我們把子類丟進父類參數的方法做運算, 似乎也很ok
1234public int Sell(Animal animal) {return animal.GetPrice();}Sell(new Dog()); - 如果我們回傳子類, 但回傳值的型別是父類, 似乎也很ok
123public Animal Buy() {return new Dog();} - 但有一個情況不太ok
12List<Animal> animals = new List<Dog>() {dog1, dog2};animals.Add(new Cat()); // 這樣真的可以嗎?
看起來操作是合法的,但是如果底層的物件是List<Dog>, 那加入Cat是不是個合法的行為,就有待商榷。
語言規範
語言的設計者必須要回答上述問題。在C# 4.0之後,語言的回答是:
- 如果你沒有對泛型參數做任何約束,那麼會採用最嚴格的不變性invariance.
123456List<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),那編譯器就允許你做你想做的事。
1IEnumerable<Animal> animals = new List<Dog>(); // Fineout
如果你去看MSDN關於
IEnumerable<T>
,會看到T前面有一個out1public interface IEnumerable<out T> : System.Collections.IEnumerable這個 out 的意思是,我們對這個T多做一些約束,對於所有該介面的方法,T只能用在回傳值
123456interface IEnumerable<out T>{public IEnumerator<T> GetEnumerator ();}public System.Collections.Generic.IEnumerator<out T> GetEnumerator ();// IEnumerable<T>只有GetEnumerator這個方法,而且該方法的T是放在回傳值而不是參數一旦我們限定T只能出現在回傳值,代表所有
interface<Child>
都可以正常被當成interface<Parent>
來使用12345interface Querable<out T> {T Query()}Query<ParentDto> query = new Query<ChildDto>(); // 實作可能是子類, 可能是父類ParentDto parent = query.Query(); // 但使用該介面的人都當成父類用in
in 是另外一種泛型約束,強調該T只能用在介面的參數,不能用在回傳值
123public interface IComparer<in T> {public int Compare (T? x, T? y);}這代表該使用
IComparer<Child>
的位置, 能夠接受傳入更一般化IComparer<Parent>
1234567public SortedSet<T>(IComparer<T>) // 這是SortedSet的constructor, 支援傳入一個IComparernew SortedSet<Circle>(new Comparer<Circle>()); // 這是常見的情況new SortedSet<Circle>(new Comparer<Shape>());// 這也沒有問題, 因為在constructor內使用IComparer的地方,// 一定會傳入Circle給Compare, 而且Circle在介面上只用於傳入值// 因此一定可以被當成Shape, 所以功能正常沒有問題無約束
而如果沒有做泛型in, out的約束, 可能就會導致型別不安全的結果。
12public class List<T> // 沒有任何in, out的約束List<Animal> animals = new List<Dog>() {dog1, dog2}; // GG, compile error在C#中,有一個因為歷史因素導致的Covariance,那就是array, 支援Covariance, 代表可以在原本的型別處使用衍伸型別(使用繼承後的子類)。設計該行為時,泛型還沒出現。
1234// compile passstring[] 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>
1234// 舉Func為例, 參數部分都加了in, 回傳值的部分加了outpublic delegate TResult Func(T arg);public delegate void Action(T1 arg1, T2 arg2, T3 arg3);public delegate bool Predicate(T obj);在使用
Func<in T,out TResult>
泛型委派的地方123456789101112IEnumerable<TSource> 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
希望這邊文章足夠深入淺出, 幫助到想了解共變數反變數的大家