一句話總結:協變讓一個粗粒度接口(或委託)能夠接收一個更加具體的接口(或委託)做爲參數(或返回值);逆變讓一個接口(或委託)的參數類型(或返回值)類型更加具體化,也就是參數類型更強,更明確。算法
一般,協變類型參數可用做委託的返回類型,而逆變類型參數可用做參數類型。對於接口,協變類型參數可用做接口的方法的返回類型,而逆變類型參數可用做接口的方法的參數類型。編程
協變數組
咱們先來看下面一個來自MSDN的例子:
01 // 協變安全
02 IEnumerable<string> strings = new List<string>();函數
03 IEnumerable<object> objects = strings;代理
04 //你們看到了麼一個聲明爲IEnumerable<string>的接口類型被賦給了一個更低級別的IEnumerable<object>.對象
05 //對,這就是協變。再來看一個例子:繼承
06 class Base接口
07 {get
08 public static void PrintBases(IEnumerable<Base> bases)
09 {
10 foreach(Base b in bases)
11 {
12 Console.WriteLine(b);
13 }
14
15 }
16 }
17
18 class Derived : Base
19 {
20 public static void Main()
21 {
22 List<Derived> dlist = new List<Derived>();
23 Derived.PrintBases(dlist);
24 //因爲IEnumerable<T>接口是協變的,因此PrintBases(IEnumerable<Base> bases)
25 //能夠接收一個更加具體化的IEnumerable<Derived>做爲其參數。
26 IEnumerable<Base> bIEnum = dlist;
27 }
28 }
下面給協變下個定義:
協變:讓一個帶有協變參數的泛型接口(或委託)能夠接收類型更加精細化,具體化的泛型接口(或委託)做爲參數,能夠當作OO中多態的一個延伸。
逆變
1 // 逆變
2 // Assume that the following method is in the class:
3 // static void SetObject(object o) { }
4 Action<object> actObject = SetObject;
5 Action<string> actString = actObject;
6 //委託actString中之後要使用更加精細化的類型string不能再使用object啦!
7 string strHello(「Hello」);
8 actString(strHello);
你們看到了麼?一個聲明爲Action<object>的類型被賦給了一個Action<string>,你們都知道,Action<T>接收參數,沒有返回值,因此其中的object和string是其參數,這個過程其實就是參數的約束更增強了,也就是說讓參數類型更加精細化。下面咱們來給逆變下個定義:
逆變:讓一個帶有協變參數的泛型接口(或委託)能夠接收粒度更粗的泛型接口或委託做爲參數,這個過程其實是參數類型更加精細化的過程。
1、兩個概念:強類型與弱類型
爲了後面敘述方便,我如今這裏自定義兩個概念:強類型和弱類型。在本篇文章中,強類型和弱類型指的是兩個具備直接或者間接繼承關係的兩個類。若是一個類是另外一個類的直接或者間接基類,那麼它爲弱類型,直接或者間接子類爲強類型。後續的介紹中會用到的兩個類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類型的變量。
2、委託中的協變與逆變的使用
協變和逆變主要體如今兩個地方:接口和委託,先來看看在委託中如何使用協變和逆變。如今咱們定義了以下一個表示無參函數的泛型委託Function,類型參數爲函數返回值的類型。泛型參數以前添加了一個out關鍵字表示T是一個協變變體。那麼在使用過程當中,基於強類型的委託Fucntion實例就能夠賦值給基於弱類型的委託Fucntion變量。
01 public delegate T Function<out T>();
02 class Program
03 {
04 static void Main()
05 {
06 Function funcBar = new Function(GetInstance);
07 Function funcFoo = funcBar;
08 Foo foo = funcFoo();
09 }
10 static Bar GetInstance()
11 {
12 return new Bar();
13 }
14 }
接下來介紹逆變委託的用法。下面定義了一個名稱爲Operate的泛型委託,接受一個具備泛型參數類型的參數。在定義泛型參數前添加了in關鍵字,表示T是一個基於逆變的變體。因爲使用了逆變,咱們就能夠將基於弱類型的委託Operate實例就能夠賦值給基於強類型的委託Operate變量。
01 public delegate void Operate<in T>(T instance);
02 class Program
03 {
04 static void Main()
05 {
06 Operate opFoo = new Operate(DoSth);
07 Operate opBar = opFoo;
08 opBar(new Bar());
09 }
10 static void DoSth(Foo foo)
11 {
12 //Others...
13 }
14 }
3、接口中的協變與逆變的使用
接下來咱們一樣經過一個簡單的例子來講明在接口中如何使用協變和逆變。下面定義了一個繼承自 IEnumerable接口的IGroup集合類型,和上面同樣,泛型參數T以前的out關鍵字代表這是一個協變。既然是協變,咱們就能夠將一個基於強類型的委託IGroup實例就能夠賦值給基於弱類型的委託IGroup變量。
01 public interface IGroup<out T> : IEnumerable
02 { }
03
04 public class Group : List, IGroup
05 { }
06
07 public delegate void Operate<in T>(T instance);
08
09 class Program
10 {
11 static void Main()
12 {
13 IGroup groupOfBar = new Group();
14 IGroup groupOfFoo = groupOfBar;
15 //Others...
16 }
17 }
下面是一個逆變接口的例子。首先定義了一個IPaintable的接口,裏面定義了一個可讀寫的Color屬性,即是實現該接口的類型的對象具備本身的顏色,並能夠改變顏色。類型Car實現了該接口。接口IBrush定義了一把刷子,泛型類型須要實現IPaintable接口,in關鍵字代表這是一個逆變。方法Paint用於將指定的對象粉刷成相應的顏色,表示被粉刷的對象的類型爲泛型參數類型。Brush實現了該接口。因爲IBrush定義成逆變,咱們就能夠將基於強類型的委託IBrush實例就能夠賦值給基於弱類型的委託IBrush變量。
public interface IPaintable
{
Color Color { get; set; }
}
public class Car : IPaintable
{
public Color Color { get; set; }
}
public interface IBrush<in T> where T : IPaintable
{
void Paint(T objectToPaint, Color color);
}
public class Brush : IBrush where T : IPaintable
{
public void Paint(T objectToPaint, Color color)
{
objectToPaint.Color = color;
}
}
class Program
{
static void Main()
{
IBrush brush = new Brush();
IBrush carBrush = brush;
Car car = new Car();
carBrush.Paint(car, Color.Red);
Console.WriteLine(car.Color.Name);
}
}4、從Func看協變與逆變的本質
接下來咱們來談談協變和逆變的本質區別是什麼。在這裏咱們以咱們很是熟悉的一個委託Func做爲例子,下面給出了該委託的定義。咱們能夠看到Func定義的兩個泛型參數分別屬於逆變和協變。具體來講輸入參數類型爲逆變,返回值類型爲協變。
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 Fucntionin TResult>(T arg);
說到這裏,我想有人要問一個問題,既然輸入表示逆變,輸出表示協變,委託的輸出參數應該定義成協變了?非也,實際上輸出參數在這裏既輸出輸出,也輸出輸入(畢竟調用的時候須要指定一個對應類型的對象)。也正是爲此,輸出參數的類型及不能定義成協變,也不能定義成逆變。因此下面兩種變體的定義也是不能經過編譯的。
1 delegate void Action<in T>(out T arg);
2 delegate void Action<out T>(out T arg);
5、逆變實現了「算法」的重用
實際上關係協變和逆變體現出來的編程思想,還有一種我比較推崇的說法,那就是:協變是繼承的體現,而逆變體現的則是多態。實際上這與上面分析的契約關係本質上是一致的。
關於逆變,在這裏請容我再囉嗦一句:逆變背後蘊藏的編程思想體現出了對算法的重用——咱們爲基類定義了一套操做,能夠自動應用於全部子類的對象。
完整示例
01 /// <summary>
02 /// 協變和逆變容許數組類型、委託類型和泛型類型參數進行隱式引用轉換。 協變保留分配兼容性,逆變與之相反。
03 /// 協變和逆變只能用於引用類型,不能用於值類型或void
04 /// </summary>
05 public class CovarianceAndContravariance : IFace
06 {
07 public CovarianceAndContravariance()
08 {
09 ///分配兼容性
10 string str = "test";
11 object obj = str;
12
13 ///數組的協變容許派生程度更大的類型的數組隱式轉換爲派生程度更小的類型的數組,可是此操做運行時不是類型安全的操做.
14 object[] array = new String[10];
15 // array[0] = 10;
16
17 ///方法的協變和逆變
18 Func<object> del = GetString;
19 //Func<string> del00 = GetObject; //返回值不能逆變
20 Action<string> del2 = SetObject;
21 //Action<object> del22 = SetString; //參數不能協變
22 Action<object> actObject = SetObject;
23 Action<string> actString = actObject;
24
25 // 泛型類型參數進行隱式引用轉換
26 IEnumerable<string> strings = new List<string>();
27 IEnumerable<object> objects = strings;
28 }
29
30 static object GetObject() { return null; }
31 static void SetObject(object obj) { }
32
33 static string GetString() { return ""; }
34 static void SetString(string str) { }
35
36 /// <summary>
37 /// 接口不存在協變和逆變
38 /// </summary>
39 /// <param name="obj"></param>
40 /// <returns></returns>
41 public string func(object obj)
42 {
43 return null;
44 }
45 public object func2(string obj)
46 {
47 return null;
48 }
49 }
50 public interface IFace
51 {
52 string func(object obj);
53 object func2(string obj);
54 }
一句話總結:協變讓一個粗粒度接口(或委託)能夠接收一個更加具體的接口(或委託)做爲參數(或返回值);逆變讓一個接口(或委託)的參數類型(或返回值)類型更加具體化,也就是參數類型更強,更明確。
一般,協變類型參數可用做委託的返回類型,而逆變類型參數可用做參數類型。對於接口,協變類型參數可用做接口的方法的返回類型,而逆變類型參數可用做接口的方法的參數類型。