簡單解釋共變數(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前面有一個outpublic 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
希望這邊文章足夠深入淺出, 幫助到想了解共變數反變數的大家