转自:http://www.cnblogs.com/jingzhongliumei/archive/2012/07/02/2573149.html

先做点准备工作,定义两个类:Animal类和其子类Dog类,一个泛型接口IMyInterface<T>, 他们的定义如下:

 
public class Animal
{
}
public class Dog : Animal
{
}
interface IMyInterface<T>

}
 

一.  协变和逆变的定义

从.Net Framework 4.0开始引入了一个新特性:协变与逆变,有人翻译为协变和反变,他们实际上所指的就是不同类型之间的一种转变(Variance). 那么具体来说什么是协变和逆变呢?

就拿普通类来做个类比吧,对于普通类来说,下面两种转换你肯定不会陌生:

Animal animal = new Dog(); //类型的隐式转换
Dog dog = (Dog)animal; //类型的强制转换

与上面两种转换相类似,从.Net4.0开始,对于泛型接口来说,下面两种转换就是协变和逆变:

IMyInterface<Animal> iAnimal = null;
IMyInterface<Dog> iDog = null;
iAnimal = iDog;// “子类”向“父类”转换,即泛型接口的协变
iDog = iAnimal;// “父类”向“子类”转换,即泛型接口的逆变

所以如果进行简单类比下这两者的定义的话就是:所谓协变就是泛型接口从子类向父类转化,所谓逆变就是父类向子类转换. 在.Net 4.0以前,是没有协变逆变的概念的,即上面两行代码中的任何一行都是不允许的, 因为虽然IMyInterface<Animal>和IMyInterface<Dog>表面上看起来有点类似父类和子类的关系, 但实际上他们根本没有任何继承上的关系. 从.Net 4.0开始, 有条件地允许上面的协变和逆变的两种转化. 这个条件就是在申明接口的时候使用in或out关键字来修饰限制泛型参数T的使用范围.

实际上,如果你在Visual Studio尝试了上面的两行协变和逆变的代码的话, 你就会发现, 那两行代码根本就不能编译通过,原因就在于我们并未按照语法所要求的那样使用修饰符in或out, 但是如果我们在泛型接口的声明中加上了in或out限制条件来修饰泛型参数T的时候,代码就可以编译通过了. 如果我们像下面这样声明接口:

interface IMyInterface<out T>
{
}

那么协变(即iAnimal = iDog;)是可以编译通过的,逆变则不行.而如果我们像这样声明接口:

interface IMyInterface<in T>
{
}

那么逆变(即iDog = iAnimal;)是可以编译通过的,协变则不行.所以我们总结起来就是: 用out来修饰泛型参数的时候则允许协变,用in来修饰泛型参数的时候则允许逆变.

那么现在你很可能会想到几个问题,为什么.Net4.0以前不支持协变和逆变呢? 为什么.Net4.0开始微软要引入协变逆变呢? in和out又代表了什么意思呢?

二.  为什么以前不支持协变和逆变

注意:以下代码只是基于假设的分析用,不能实际编译和执行.

为什么.Net 4.0以前不支持协变和逆变呢? 还是以本文开头的准备工作中的两个类一个接口为例, 不过那个接口需修改一下,给它增加两个方法,如下:

interface IMyInterface<T>
{
    void ShowMe(T t);
    T GetMe();
}

如果允许协变的话,那么在调用ShowMe方法的时候就可能出现问题, 请考虑如下代码:

Animal animal=new Animal();
IMyInterface<Animal> iAnimal = null;
IMyInterface<Dog> iDog = null;
iAnimal = iDog;
iAnimal.ShowMe(animal); 

我们在写iAnimal.ShowMe(animal)这行代码的时候,Visual Studio按照IMyInterface<Animal>来进行代码提示,如下图所示

Visual Studio要我们输入Animal类型的对象,但是在运行时执行ShowMe方法的时候, 因为实际对象是IMyInterface<Dog>,所以实际执行的方法是ShowMe(Dog t)方法, 所以最终就有可能导致用一个Animal的实例去调用ShowMe(Dog t)方法,这显然是错误的!

与上面对协变的分析类似,再来看逆变, 如果允许逆变的话,那么在调用GetMe方法的时候就可能出现问题,代码如下:

Dog animal=new Dog ();
IMyInterface<Animal> iAnimal = null;
IMyInterface<Dog> iDog = null;
iDog = iAnimal;
Dog dog=iDog.GetMe();

我们在写Dog dog=iDog.GetMe()这行代码的时候,Visual Studio按照IMyInterface<Dog>来进行代码提示,如下图所示

Visual Studio提示返回Dog类型的对象,但是在运行时执行GetMe方法的时候, 因为实际对象是IMyInterface<Animal>,所以实际执行的方法GetMe()的返回值为Animal, 所以最终就有可能导致用一个Animal的实例去赋值给dog,这显然也是错误的!

通过上面的分析我们知道, 如果允许协变的话,那么可能会导致在有泛型输入参数的方法在运行时出错, 如果允许逆变的话, 则有可能导致在有泛型返回值的方法在运行时出错. 由此可见,泛型参数T用在方法的输出参数还是输入参数决定了这个泛型接口是支持协变还是逆变. 归根结底, 无论是协变问题还是逆变问题都是因为这样的一个原则: 子类可以向父类隐式转换但是父类不能向子类隐式转换. 协变和逆变的问题只是这个原则变化了一个伪装外衣而已.

三.  为什么.Net4.0开始要引入协变逆变以及inout的用法

如果没有协变的情况下,假设有这样一个场景,我们需要将IEnumerable<Animal>和IEnumerable<Dog>合并成List<Animal>,代码如下:

IEnumerable<Animal> animals = GetAnimals();
IEnumerable<Dog> dogs = GetDogs();
List<Animal> list = new List<Animal>();
list.AddRange(animals);
list.AddRange(dogs); //因为没有协变,所以这行代码编译报错

上面的最后一行代码会编译报错,因为没有协变,所以就不能用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仍然可以同时用于输入参数和输出参数的. 除了泛型接口外, 泛型委托也有协变和逆变的问题, 正如本文中所提到的那样, 泛型接口也好,泛型委托也罢, 甚至包括逆变协变对象作为方法参数的时候, 他们的协变逆变的问题实际上都源于一个根本的原则: 子类可以向父类隐式转换但是父类不能向子类隐式转换.

.NET 4.0中的泛型的协变和逆变的更多相关文章

  1. 转载.NET 4.0中的泛型的协变和逆变

    先做点准备工作,定义两个类:Animal类和其子类Dog类,一个泛型接口IMyInterface<T>, 他们的定义如下:   public class Animal { } public ...

  2. Java泛型的协变与逆变

    泛型擦除 Java的泛型本质上不是真正的泛型,而是利用了类型擦除(type erasure),比如下面的代码就会出现错误: 报的错误是:both methods  have same erasure ...

  3. Kotlin泛型与协变及逆变原理剖析

    在上一次https://www.cnblogs.com/webor2006/p/11234941.html中学习了数据类[data class]相关的知识,这次会学习关于泛型相关的东东,其中有关于泛型 ...

  4. Java用通配符 获得泛型的协变和逆变

    Java对应泛型的协变和逆变

  5. C#4.0泛型的协变,逆变深入剖析

    C#4.0中有一个新特性:协变与逆变.可能很多人在开发过程中不常用到,但是深入的了解他们,肯定是有好处的. 协变和逆变体现在泛型的接口和委托上面,也就是对泛型参数的声明,可以声明为协变,或者逆变.什么 ...

  6. C# 泛型的协变和逆变

    1. 可变性的类型:协变性和逆变性 可变性是以一种类型安全的方式,将一个对象当做另一个对象来使用.如果不能将一个类型替换为另一个类型,那么这个类型就称之为:不变量.协变和逆变是两个相互对立的概念: 如 ...

  7. C# 泛型的协变和逆变 (转载)

    1. 可变性的类型:协变性和逆变性 可变性是以一种类型安全的方式,将一个对象当做另一个对象来使用.如果不能将一个类型替换为另一个类型,那么这个类型就称之为:不变量. 协变和逆变是两个相互对立的概念: ...

  8. C#-弄懂泛型和协变、逆变

    脑图概览 泛型声明和使用 协变和逆变 <C#权威指南>上在委托篇中这样定义: 协变:委托方法的返回值类型直接或者间接地继承自委托前面的返回值类型; 逆变:委托签名中的参数类型继承自委托方法 ...

  9. c#-泛型、协变、逆变

    泛型简单介绍: 可以使用泛型声明的元素:类.接口.方法.委托 泛型之前:泛型之前使用object封装不同类型的参数,缺点:性能差.运行时判断类型(不安全)...泛型是在编译期间转为实际类型副本,所以性 ...

随机推荐

  1. 微信公众平台开发(十) 消息回复总结——用其xml模板

    一.简介 微信公众平台提供了三种消息回复的格式,即文本回复.音乐回复和图文回复,在这一篇文章中,我们将对这三种消息回复的格式做一下简单讲解,然后封装成函数,以供读者使用. 二.思路分析 对于每一个PO ...

  2. Win10走红背后,最开心的人却是谷歌

    导读 微软在不惜余力推进Windows10普及的同时,也有一些让自己小小郁闷的事儿发生,在Win10系统当中,微软用新的Edge浏览器取代了用户熟悉的IE浏览器,以求改写在浏览器市场上的被动局面,不过 ...

  3. 【Spring】Spring系列2之bean的配置

    2.bean的配置 2.1.IOC概述 2.2.bean的获取 2.3.依赖注入方式 2.4.属性注入细节 内部bean,不需要ID,ID无效,外部不能引用: 2.5.集合属性注入 2.6.使用p命名 ...

  4. windows和linux下mysql的重启命令

    在 Windows 下: 开始->运行->cmd启动:net start mysql停止:net stop mysql 无重启,必须先停止再启动!!! 在LINUX 下: 启动:/etc/ ...

  5. 5.django笔记之form保存表单信息,动态select

    作者:刘耀 一.使用form保存用户输入过的信息 场景:例如 如果用户注册,那么他输入n多个表单之后,那么他提交是时候,如果错误返回的时候,那么需要重新再输入表单内容.这样会影响用户体验,所以,使用f ...

  6. 前台js分页,自己手写逻辑

    js代码如下: //设置分页 var pageSize = 10; //设置一次显示多少页 var pageLimit = 5; $(function(){ //查询所有内容 $.post(ctx + ...

  7. [SVN(ubuntu)] svn 文件状态标记含义

    A item 文件.目录或是符号链item预定加入到版本库. C item 文件item发生冲突,在从服务器更新时与本地版本发生交迭,在你提交到版本库前,必须手工的解决冲突. D item 文件.目录 ...

  8. BST树

    http://www.cnblogs.com/bizhu/archive/2012/08/19/2646328.html 4. 二叉查找树(BST) Technorati 标记: 二叉查找树,BST, ...

  9. Set和Map

    Set和Map

  10. Redis笔记(三)Redis的数据类型

    前面说过,Redis的一大特性是支持丰富的数据类型, 这为更多的应用场景提供了可能. Redis有五种数据类型,包括string,list,set,sorted set和hash,注意,Redis的数 ...