導讀:作者蔣金楠,網名Artech。解決方案架構與互聯絡統MVP,微軟最有影響力開發人員。在《談談C# 4.0新特性“預設參數”的實現》中我們揭示了“預設參數”的本質,現在我們接著來談談C#4.0中另一個重要的新特性:協變(Covariance)與逆變(Contravariance)。對於協變與逆變,大家肯定不會感到陌生,但是我相信有很多人不能很清晰地說出他們之間的區別。我希望通過這篇文章能夠讓讀者更加深刻的認識協變與逆變。以下是文章內容:
一、兩個概念:強型別與弱類型
為了後面敘述方便,我現在這裡自訂兩個概念:強型別和弱類型。在本篇文章中,強型別和弱類型指的是兩個具有直接或者間接繼承關係的兩個類。如果一個類是另一個類的直接或者間接基類,那麼它為弱類型,直接或者間接子類為強型別。後續的介紹中會用到的兩個類Foo和Bar先定義在這裡。Bar繼承自Foo。 Foo是弱類型,而Bar則是強型別。
1: public class Foo
2: {
3: //Others Members...
4: }
5: public class Bar:Foo
6: {
7: //Others Members...
8: }
有了強型別和弱類型的概念,我們就可以這樣的定義協變和逆變:如果類型TBar是基於強型別Bar的類型(比如型別參數為Bar的泛型型別,或者是參數/傳回值類型為Bar的委託),而類型TFoo是基於弱類型Foo的類型,協變就是將TBar類型的執行個體賦值給TFoo類型的變數,而逆變則是將TFoo類型的執行個體賦值給TBar類型的變數。
二、委託中的協變與逆變的使用
協變和逆變主要體現在兩個地方:介面和委託,先來看看在委託中如何使用協變和逆變。現在我們定義了如下一個表示無參函數的泛型委派Function<T>,型別參數為函數傳回值的類型。泛型參數之前添加了一個out關鍵字表示T是一個協變變體。那麼在使用過程中,基於強型別的委託Fucntion<Bar>執行個體就可以賦值給基於弱類型的委託 Fucntion<Foo>變數。
1: public delegate T Function<out T>();
2: class Program
3: {
4: static void Main()
5: {
6: Function<Bar> funcBar = new Function<Bar>(GetInstance);
7: Function<Foo> funcFoo = funcBar;
8: Foo foo = funcFoo();
9: }
10: static Bar GetInstance()
11: {
12: return new Bar();
13: }
14: }
接下來介紹逆變委託的用法。下面定義了一個名稱為Operate的泛型委派,接受一個具有泛型參數類型的參數。在定義泛型參數前添加了in關鍵字,表示T是一個基於逆變的變體。由於使用了逆變,我們就可以將基於弱類型的委託 Operate<Foo>執行個體就可以賦值給基於強型別的委託Operate<Bar>變數。
1: public delegate void Operate<in T>(T instance);
2: class Program
3: {
4: static void Main()
5: {
6: Operate<Foo> opFoo = new Operate<Foo>(DoSth);
7: Operate<Bar> opBar = opFoo;
8: opBar(new Bar());
9: }
10: static void DoSth(Foo foo)
11: {
12: //Others...
13: }
14: }
三、介面中的協變與逆變的使用
接下來我們同樣通過一個簡單的例子來說明在介面中如何使用協變和逆變。下面定義了一個繼承自 IEnumerable<T>介面的IGroup<out T>集合類型,和上面一樣,泛型參數T之前的out關鍵字表明這是一個協變。既然是協變,我們就可以將一個基於強型別的委託 IGroup<Bar>執行個體就可以賦值給基於弱類型的委託IGroup<Foo>變數。
1: public interface IGroup<out T> : IEnumerable<T>
2: { }
3: public class Group<T> : List<T>, IGroup<T>
4: { }
5: public delegate void Operate<in T>(T instance);
6: class Program
7: {
8: static void Main()
9: {
10: IGroup<Bar> groupOfBar = new Group<Bar>();
11: IGroup<Foo> groupOfFoo = groupOfBar;
12: //Others...
13: }
14: }
下面是一個逆變介面的例子。首先定義了一個IPaintable的介面,裡面定義了一個可讀寫的 Color屬性,便是實現該介面的類型的對象具有自己的顏色,並可以改變顏色。類型Car實現了該介面。介面IBrush<in T>定義了一把刷子,泛型型別需要實現IPaintable介面,in關鍵字表明這是一個逆變。方法Paint用於將指定的對象粉刷成相應的顏色,表示被粉刷的對象的類型為泛型參數類型。Brush<T>實現了該介面。由於IBrush<in T>定義成逆變,我們就可以將基於強型別的委託IBrush<Car>執行個體就可以賦值給基於弱類型的委託 IBrush<IPaintable>變數。
1: public interface IPaintable
2: {
3: Color Color { get; set; }
4: }
5: public class Car : IPaintable
6: {
7: public Color Color { get; set; }
8: }
9:
10: public interface IBrush<in T> where T : IPaintable
11: {
12: void Paint(T objectToPaint, Color color);
13: }
14: public class Brush<T> : IBrush<T> where T : IPaintable
15: {
16: public void Paint(T objectToPaint, Color color)
17: {
18: objectToPaint.Color = color;
19: }
20: }
21:
22: class Program
23: {
24: static void Main()
25: {
26: IBrush<IPaintable> brush = new Brush<IPaintable>();
27: IBrush<Car> carBrush = brush;
28: Car car = new Car();
29: carBrush.Paint(car, Color.Red);
30: Console.WriteLine(car.Color.Name);
31: }
32: }
四、從Func<T,TResult>看協變與逆變的本質
接下來我們來談談協變和逆變的本質區別是什麼。在這裡我們以我們非常熟悉的一個委託 Func<T, TResult>作為例子,下面給出了該委託的定義。我們可以看到Func<T, TResult>定義的兩個泛型參數分別屬於逆變和協變。具體來說輸入參數類型為逆變,傳回值類型為協變。
1: public delegate TResult Func<in T, out TResult>(T arg);
再重申以下這句話“輸入參數類型為逆變,傳回值類型為協變”。然後,你再想想為什麼逆變用in關鍵字,而協變用out關鍵字。這兩個不是偶然,實際上我們可以將協變/逆變與輸出/輸入匹配起來。
我們再從另一個角度來理解協變與逆變。我們知道介面代表一種契約,當一個類型實現一個介面的時候就相當於簽署了這份契約,所以必須是實現介面中所有的成員。實際上類型繼承也屬於一種契約關係,基類定義契約,子類“簽署”該契約。對於類型系統來說,介面實現和類型繼承本質上是一致的。契約是弱類型,簽署這份契約的是強型別。
將契約的觀點應用在委託上面,委託實際上定義了一個方法的簽名(參數列表和傳回值),那麼參數和傳回值的類型就是契約,現在的關鍵是誰去履行這份契約。所有參數是外界傳入的,所以基於參數的契約履行者來源於外部,也就是被賦值變數的類型,所以被賦值變數類型是強型別。而對於代理本身來說,參數是一種輸入,也就是一種採用in關鍵字表示的逆變。
而對於委託的傳回值,這是給外部服務的,是委託自身對外界的一種承諾,所以它自己是契約的履行著,因此它自己應該是強型別。相應地,對於代理本身來說,傳回值是一種輸出,也就是一種採用out關鍵字定義的協變。
也正式因為這個原因,對於一個委託,你不能將參數類型定義成成協變,也不能將傳回型別定義成逆變。下面兩中變體定義方式都是不能通過編譯的。
1: delegate TResult Fucntion<out T, TResult>(T arg);
2: delegate TResult Fucntion<T, in TResult>(T arg);
說到這裡,我想有人要問一個問題,既然輸入表示逆變,輸出表示協變,委託的輸出參數應該定義成協變了?非也,實際上輸出參數在這裡既輸出輸出,也輸出輸入(畢竟調用的時候需要指定一個對應類型的對象)。也正是為此,輸出參數的類型及不能定義成協變,也不能定義成逆變。所以下面兩種變體的定義也是不能通過編譯的。
1: delegate void Action<in T>(out T arg);
2: delegate void Action<out T>(out T arg);
雖然這裡指介紹了關於委託的協變與逆變,上面提到的契約和輸入/輸出的關係也同樣適用於基於介面的協變與逆變。你自己可以採用這樣的方式去分析上面一部分我們定義的IGroup<Foo>和IBrush<in T>。
五、逆變實現了“演算法”的重用
實際上關係協變和逆變體現出來的編程思想,還有一種我比較推崇的說法,那就是:協變是繼承的體現,而逆變體現的則是多態。實際上這與上面分析的契約關係本質上是一致的。
關於逆變,在這裡請容我再囉嗦一句:逆變背後蘊藏的編程思想體現出了對演算法的重用——我們為基類定義了一套操作,可以自動應用於所有子類的對象。
原文連結:http://www.cnblogs.com/artech/archive/2011/01/13/1934914.html