转自:http://www.cnblogs.com/jingzhongliumei/archive/2012/07/02/2573149.htmlhtml
先作点准备工做,定义两个类:Animal类和其子类Dog类,一个泛型接口IMyInterface<T>, 他们的定义以下:spa
一. 协变和逆变的定义翻译
从.Net Framework 4.0开始引入了一个新特性:协变与逆变,有人翻译为协变和反变,他们实际上所指的就是不一样类型之间的一种转变(Variance). 那么具体来讲什么是协变和逆变呢?code
就拿普通类来作个类比吧,对于普通类来讲,下面两种转换你确定不会陌生:htm
与上面两种转换相相似,从.Net4.0开始,对于泛型接口来讲,下面两种转换就是协变和逆变:对象
因此若是进行简单类比下这二者的定义的话就是:所谓协变就是泛型接口从子类向父类转化,所谓逆变就是父类向子类转换. 在.Net 4.0之前,是没有协变逆变的概念的,即上面两行代码中的任何一行都是不容许的, 由于虽然IMyInterface<Animal>和IMyInterface<Dog>表面上看起来有点相似父类和子类的关系, 但实际上他们根本没有任何继承上的关系. 从.Net 4.0开始, 有条件地容许上面的协变和逆变的两种转化. 这个条件就是在申明接口的时候使用in或out关键字来修饰限制泛型参数T的使用范围.blog
实际上,若是你在Visual Studio尝试了上面的两行协变和逆变的代码的话, 你就会发现, 那两行代码根本就不能编译经过,缘由就在于咱们并未按照语法所要求的那样使用修饰符in或out, 可是若是咱们在泛型接口的声明中加上了in或out限制条件来修饰泛型参数T的时候,代码就能够编译经过了. 若是咱们像下面这样声明接口:继承
那么协变(即iAnimal = iDog;)是能够编译经过的,逆变则不行.而若是咱们像这样声明接口:接口
那么逆变(即iDog = iAnimal;)是能够编译经过的,协变则不行.因此咱们总结起来就是: 用out来修饰泛型参数的时候则容许协变,用in来修饰泛型参数的时候则容许逆变.get
那么如今你极可能会想到几个问题,为何.Net4.0之前不支持协变和逆变呢? 为何.Net4.0开始微软要引入协变逆变呢? in和out又表明了什么意思呢?
二. 为何之前不支持协变和逆变
注意:如下代码只是基于假设的分析用,不能实际编译和执行.
为何.Net 4.0之前不支持协变和逆变呢? 仍是以本文开头的准备工做中的两个类一个接口为例, 不过那个接口需修改一下,给它增长两个方法,以下:
若是容许协变的话,那么在调用ShowMe方法的时候就可能出现问题, 请考虑以下代码:
咱们在写iAnimal.ShowMe(animal)这行代码的时候,Visual Studio按照IMyInterface<Animal>来进行代码提示,以下图所示
Visual Studio要咱们输入Animal类型的对象,可是在运行时执行ShowMe方法的时候, 由于实际对象是IMyInterface<Dog>,因此实际执行的方法是ShowMe(Dog t)方法, 因此最终就有可能致使用一个Animal的实例去调用ShowMe(Dog t)方法,这显然是错误的!
与上面对协变的分析相似,再来看逆变, 若是容许逆变的话,那么在调用GetMe方法的时候就可能出现问题,代码以下:
咱们在写Dog dog=iDog.GetMe()这行代码的时候,Visual Studio按照IMyInterface<Dog>来进行代码提示,以下图所示
Visual Studio提示返回Dog类型的对象,可是在运行时执行GetMe方法的时候, 由于实际对象是IMyInterface<Animal>,因此实际执行的方法GetMe()的返回值为Animal, 因此最终就有可能致使用一个Animal的实例去赋值给dog,这显然也是错误的!
经过上面的分析咱们知道, 若是容许协变的话,那么可能会致使在有泛型输入参数的方法在运行时出错, 若是容许逆变的话, 则有可能致使在有泛型返回值的方法在运行时出错. 因而可知,泛型参数T用在方法的输出参数仍是输入参数决定了这个泛型接口是支持协变仍是逆变. 归根结底, 不管是协变问题仍是逆变问题都是由于这样的一个原则: 子类能够向父类隐式转换, 可是父类不能向子类隐式转换. 协变和逆变的问题只是这个原则变化了一个假装外衣而已.
三. 为何.Net4.0开始要引入协变逆变, 以及in和out的用法
若是没有协变的状况下,假设有这样一个场景,咱们须要将IEnumerable<Animal>和IEnumerable<Dog>合并成List<Animal>,代码以下:
上面的最后一行代码会编译报错,由于没有协变,因此就不能用IEnumerable<Dog>做为参数去调用要求参数为IEnumerable<Animal>, 那么咱们就不得不为此写一个循环, 把IEnumerable<Dog>中的Dog都提取出来,隐式转换为Animal再一个一个加入到list中去. 是否是以为有点麻烦了, 明明就只是把Dog转化为Animal, 为何微软你就不能代劳一下呢?
由此看来, 支持协变和逆变确实能让咱们方便地写出更简洁优雅的代码, 而为了不出现上文中所讨论的错误,必须对泛型参数T的使用范围进行限制, 因此引入了in和out修饰符, 从.Net4.0开始,用in来修饰泛型参数T的时候, 表示T只能用于方法的输入参数, 此时参数T是逆变的, 若是你将T用于输出参数的话就会编译报错, 同理, out所修饰的T只能用于输出参数,此时参数T是协变的. 而且,微软改写了不少的原来的泛型接口, 尽量地加上了in或out修饰符, 让这些泛型接口支持逆变或者协变, 例如将IEnumerable<T>从新声明为IEnumerable<out T>。. 因此在.Net4.0中, 上面的那行代码list.AddRange(dogs)就再也不会编译报错了.
四. 总结
经过上面的一些简单示例代码和说明, 相信你们对泛型的协变和逆变应该有了一个基本的了解. 协变和逆变的引入让咱们可在不一样的泛型接口之间能够相互赋值, 它提供了一种相似多态的特性, 增长了灵活性, 极大地方便了代码的编写. 可是同时也在必定程度上限制了泛型参数T使用的自由度, 被in或out修饰的泛型参数T将只能用于输入参数或者输出参数. 对于只须要输入或者只须要输出的泛型接口来讲无疑是有利无弊的. 固然, 若是咱们不加修饰符in或out, 则T仍然能够同时用于输入参数和输出参数的. 除了泛型接口外, 泛型委托也有协变和逆变的问题, 正如本文中所提到的那样, 泛型接口也好,泛型委托也罢, 甚至包括逆变协变对象做为方法参数的时候, 他们的协变逆变的问题实际上都源于一个根本的原则: 子类能够向父类隐式转换, 可是父类不能向子类隐式转换.