先做点准备工作,定义两个类: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中的泛型的协变和逆变

    转自:http://www.cnblogs.com/jingzhongliumei/archive/2012/07/02/2573149.html 先做点准备工作,定义两个类:Animal类和其子类D ...

  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. POJ2418——Hardwood Species(map映射)

    Hardwood Species DescriptionHardwoods are the botanical group of trees that have broad leaves, produ ...

  2. 面试常考的数据结构Java实现

    1.线性表 2.线性链表 3.栈 4.队列 5.串 6.数组 7.广义表 8.树和二叉树 的结点),并且,二叉树的子树有左右之分,其次序不能任意颠倒. 二叉树的性质: :在二叉树的第 i 层上至多有2 ...

  3. R语言学习笔记:查看函数的R源代码

    getAnywhere 该函数可以返回一些函数的R源代码,如: getAnywhere(kmeans) 该函数具体用法,请参看官方说明. Retrieve an R Object, Including ...

  4. lighttpd 介绍及安装

    一,为什么要使用lighttpd?    apache不可以吗?    在支持纯静态的对象时,比如图片,文件等 ,    lighttpd速度更快,更理想 (lighttp 图片处理好,nginx负载 ...

  5. C#中配置文件的使用

    1. 向项目添加app.config文件: 右击项目名称,选择“添加”→“添加新建项”,在出现的“添加新项”对话框中,选择“添加应用程序配置文件”:如果项目以前没有配置文件,则默认的文件名称为“app ...

  6. Android开发之ContentValues

    SQLite数据库进行CRUD的时候, 用到了ContentValues类,负责存储名值对,名都是String类型,值都是基本类型. 例子: ContentValues values=new Cont ...

  7. poj 1159 Palindrome(dp)

    题目:http://poj.org/problem?id=1159 #include<iostream> #include<cstring> #include<cstdi ...

  8. vfp 操作excel

    VFP全面控制EXCEL 收藏 VFP和Excel都可以用来进行处理数据库表格,如果巧妙地将二者的优点结合起来,将会大大方便我们的工作.比如我们可以利用VFP进行处理数据,而利用Excel的预览打印功 ...

  9. UVa 11040 (水题) Add bricks in the wall

    题意: 45块石头如图排列,每块石头上的数等于下面支撑它的两数之和,求其余未表示的数. 分析: 首先来计算最下面一行的数,A71 = A81 + A82 = A91 + 2A92 + A93,变形得到 ...

  10. HAOI2007反素数

    1053: [HAOI2007]反素数ant Time Limit: 10 Sec  Memory Limit: 162 MBSubmit: 1346  Solved: 732[Submit][Sta ...