逆变(contravariant)与协变(covariant)
逆变(contravariant)与协变(covariant)是C#4新增的概念,许多书籍和博客都有讲解,我觉得都没有把它们讲清楚,搞明白了它们,可以更准确地去定义泛型委托和接口,这里我尝试画图详细解析逆变与协变。
变的概念
我们都知道.Net里或者说在OO的世界里,可以安全地把子类的引用赋给父类引用,例如:
|
1
2
3
|
//父类 = 子类string str = "string";object obj = str;//变了 |
而C#里又有泛型的概念,泛型是对类型系统的进一步抽象,比上面简单的类型高级,把上面的变化体现在泛型的参数上就是我们所说的逆变与协变的概念。通过在泛型参数上使用in或out关键字,可以得到逆变或协变的能力。下面是一些对比的例子:
协变(Foo<父类> = Foo<子类> ):
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
//泛型委托:public delegate T MyFuncA<T>();//不支持逆变与协变public delegate T MyFuncB<out T>();//支持协变MyFuncA<object> funcAObject = null;MyFuncA<string> funcAString = null;MyFuncB<object> funcBObject = null;MyFuncB<string> funcBString = null;MyFuncB<int> funcBInt = null;funcAObject = funcAString;//编译失败,MyFuncA不支持逆变与协变funcBObject = funcBString;//变了,协变funcBObject = funcBInt;//编译失败,值类型不参与协变或逆变//泛型接口public interface IFlyA<T> { }//不支持逆变与协变public interface IFlyB<out T> { }//支持协变IFlyA<object> flyAObject = null;IFlyA<string> flyAString = null;IFlyB<object> flyBObject = null;IFlyB<string> flyBString = null;IFlyB<int> flyBInt = null;flyAObject = flyAString;//编译失败,IFlyA不支持逆变与协变flyBObject = flyBString;//变了,协变flyBObject = flyBInt;//编译失败,值类型不参与协变或逆变//数组:string[] strings = new string[] { "string" };object[] objects = strings; |
逆变(Foo<子类> = Foo<父类>)
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
public delegate void MyActionA<T>(T param);//不支持逆变与协变public delegate void MyActionB<in T>(T param);//支持逆变public interface IPlayA<T> { }//不支持逆变与协变public interface IPlayB<in T> { }//支持逆变MyActionA<object> actionAObject = null;MyActionA<string> actionAString = null;MyActionB<object> actionBObject = null;MyActionB<string> actionBString = null;actionAString = actionAObject;//MyActionA不支持逆变与协变,编译失败actionBString = actionBObject;//变了,逆变IPlayA<object> playAObject = null;IPlayA<string> playAString = null;IPlayB<object> playBObject = null;IPlayB<string> playBString = null;playAString = playAObject;//IPlayA不支持逆变与协变,编译失败playBString = playBObject;//变了,逆变 |
来到这里我们看到有的能变,有的不能变,要知道以下几点:
- 以前的泛型系统(或者说没有in/out关键字时),是不能“变”的,无论是“逆”还是“顺(协)”。
- 当前仅支持接口和委托的逆变与协变 ,不支持类和方法。但数组也有协变性。
- 值类型不参与逆变与协变。
那么in/out是什么意思呢?为什么加了它们就有了“变”的能力,是不是我们定义泛型委托或者接口都应该添加它们呢?
原来,在泛型参数上添加了in关键字作为泛型修饰符的话,那么那个泛型参数就只能用作方法的输入参数,或者只写属性的参数,不能作为方法返回值等,总之就是只能是“入”,不能出。out关键字反之。
当尝试编译下面这个把in泛型参数用作方法返回值的泛型接口时:
|
1
2
3
4
|
public interface IPlayB<in T>{ T Test();} |
出现了如下编译错误:
错误 1 方差无效: 类型参数“T”必须为“CovarianceAndContravariance.IPlayB<T>.Test()”上有效的 协变式。“T”为 逆变。
到这里,我们大致知道了逆变与协变的相关概念,那么为什么把泛型参数限制为in或者out就可以“变”呢?下面尝试画图解释原理。
协变不是理所当然的,逆变也没有“逆”
我们先来看看不支持逆变与协变的泛型,把子类赋给父类,再执行父类方法的具体流程,对于这样一个简单的例子的Test方法:
|
1
2
3
4
5
6
7
8
9
10
|
public interface Base<T>{ T Test(T param);}public class Sub<T> : Base<T>{ public T Test(T param) { return default(T); }}Base<string> b = new Sub<string>();b.Test(""); |
它实际的流程是这样的:

即调用父类的方法,其实实际是调用子类的方法。可以看到,这个方法能够安全的调用,需要两个条件:1.变式(父)的方法参数能安全转为原式(子)的参数;2.原式(子)的返回值能安全的转为变式的返回值。不幸的是参数的流向跟返回值的流向是相反的,所以对于既是in,又是out的泛型参数来说,肯定是行不通的,其中一个方向必然不能安全转换的。例如,对上面的例子,我们尝试“变”:
|
1
2
3
4
|
Base<object> BaseObject = null;Base<string> BaseString = null;BaseObject = BaseString;//编译失败BaseObject.Test(""); |
这里的“实际流程”如下,可以看到,参数那里是object是不能安全转换为string,所以编译失败:

看到这里如果都明白的话,我们不难得到逆变与协变的”实际流程图”(记住,它们是有in/out限制的):

可以看到,从”实际流程图”来看,逆变根本没有“逆”,都离不开只能安全地把子类的引用赋给父类引用这个根本。
来到这里应该基本理解逆变与协变了,不过装配脑袋的这篇文章有个更高级的问题,原文也有解答,这里我用上面画图的方式去理解它。
图解逆变与协变的相互作用
问题的提出,你知道那个正确吗?
|
1
2
3
4
5
6
7
8
9
10
11
|
public interface IBar<in T> { }//应该是inpublic interface IFoo<in T>{ void Test(IBar<T> bar);}//还是outpublic interface IFoo<out T>{ void Test(IBar<T> bar);} |
答案是,如果是in的话,会编译失败,out才正确(当然不要泛型修饰符也能通过编译,但IFoo就没有协变能力了)。这里的意思就是说,一个有协变(逆变)能力的泛型(IBar),作为另一个泛型(IFoo)的参数时,影响到了它(IFoo)的泛型的定义。乍一看以为是in的其中一个陷阱是T是在Test方法的参数里的,所以以为是in。但这里Test的参数根本不是T,而是IBar<T>。
我们画个图来理解它。既然out可以通过,那么它的“协变流程图”应该如下:

图跟前面那些大致一样,但理解它要跟问题相反(上面问题是先定义好IBar,再去定义IFoo)。1.我们定义好一个有协变能力的IFoo,这是前提。2.可以推出,上面的流程是成立的。3.这个流程重点是参数流向,要使整个流程成立,就必须使IBar<string> = IBar<object>成立,这不就是逆变吗?整个结论就是,有协变能力的IFoo要求它的泛型参数(IBar)有逆变能力。其实根据上面的箭头也可以理解,因为原式和变式的变向跟参数的变向是相反的,导致了它们要有相反的能力,这就是装配脑袋文章说的:方法参数的协变-反变互换原则。根据这个原理,也很容易得出,如果Test方法的返回值是IBar<T>,而不是参数,那么就要求IBar<T>要有协变能力,因为返回值的箭头与原式和变式的变向的箭头是同向的。
The End!
逆变(contravariant)与协变(covariant)的更多相关文章
- 逆变(contravariant)与协变(covariant):只能用在接口和委托上面
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.T ...
- 协变(covariant)和逆变(contravariant)
我们知道子类转换到父类,在C#中是能够隐式转换的.这种子类到父类的转换就是协变. 而另外一种类似于父类转向子类的变换,可以简单的理解为“逆变”. 上面对逆变的简单理解有些牵强,因为协变和逆变只能针对接 ...
- 不变(Invariant), 协变(Covarinat), 逆变(Contravariant) : 一个程序猿进化的故事
阿袁工作的第1天: 不变(Invariant), 协变(Covarinat), 逆变(Contravariant)的初次约 阿袁,早!开始工作吧. 阿袁在笔记上写下今天工作清单: 实现一个scala类 ...
- Java逆变(Covariant)和协变(Contravariant)
1. 定义 逆变和协变描述的经过类型变换后的类型之间的关系.假如A和B表示类型,f表示类型变换,A ≤B表示A是B的子类型,那么 如果A ≤B,f(A) ≤f(B),那么f是协变 如果A ≤B,f(B ...
- Scala中的协变,逆变,上界,下界等
Scala中的协变,逆变,上界,下界等 目录 [−] Java中的协变和逆变 Scala的协变 Scala的逆变 下界lower bounds 上界upper bounds 综合协变,逆变,上界,下界 ...
- 协变(covariance),逆变(contravariance)与不变(invariance)
协变,逆变与不变 能在使用父类型的场景中改用子类型的被称为协变. 能在使用子类型的场景中改用父类型的被称为逆变. 不能做到以上两点的被称为不变. 以上的场景通常包括数组,继承和泛型. 协变逆变与泛型( ...
- Scala 基础(十六):泛型、类型约束-上界(Upper Bounds)/下界(lower bounds)、视图界定(View bounds)、上下文界定(Context bounds)、协变、逆变和不变
1 泛型 1)如果我们要求函数的参数可以接受任意类型.可以使用泛型,这个类型可以代表任意的数据类型. 2)例如 List,在创建 List 时,可以传入整型.字符串.浮点数等等任意类型.那是因为 Li ...
- C#中的协变OUT和逆变
泛型接口和泛型委托中经常使用可变性 in 逆变,out 协变 从 list<string>转到list<object> 称为协变 (string 从object 派生,那么 ...
- .NET泛型03,泛型类型的转换,协变和逆变
协变(Convariant)和逆变(Contravariant)的出现,使数组.委托.泛型类型的隐式转换变得可能. 子类转换成基类,称之为协变:基类转换成子类,称之为逆变..NET4.0以来,支持了泛 ...
- scala 学习: 逆变和协变
scala 逆变和协变的概念网上有很多解释, 总结一句话就是 参数是逆变的或者不变的,返回值是协变的或者不变的. 但是为什么是这样的? 协变: 当s 是A的子类, 那么func(s) 是func(A) ...
随机推荐
- day9:vcp考试
Q161. An administrator wants to select a Host Power Management Policy for an ESXi 6.x host that will ...
- 836. Rectangle Overlap 矩形重叠
[抄题]: A rectangle is represented as a list [x1, y1, x2, y2], where (x1, y1) are the coordinates of i ...
- WPF之DataGrid控件根据某列的值设置行的前景色(色
一种方法是 使用 datagrid的LoadingRow事件: private void DataGrid_LoadingRow(object sender, DataGridRowEventArgs ...
- advance shading——菲涅耳现象
(计算光照的时候,从两点出发考虑,光的传播方向,以及光的在这个方向上的能量.) 光与表面交互的模型包含两类:和物体表面(surface)以及和物体的内部(body).而subsurface指的是在物体 ...
- 理解OAuth 2.0 (摘自阮一峰网络日志)
OAuth是一个关于授权(authorization)的开放网络标准,在全世界得到广泛应用,目前的版本是2.0版. 本文对OAuth 2.0的设计思路和运行流程,做一个简明通俗的解释,主要参考材料为R ...
- Netty 零拷贝(一)NIO 对零拷贝的支持
Netty 零拷贝(二)NIO 对零拷贝的支持 Netty 系列目录 (https://www.cnblogs.com/binarylei/p/10117436.html) 非直接缓冲区(HeapBy ...
- 设计模式之flyweight享元模式
运用共享技术支持大量细粒度对象的使用 Flyweight模式(享元) Java深入到一定程度,就不可避免的碰到设计模式这一概念,了解设计模式,将使自己对java中的接口或抽象类应用有更深的理解.设计模 ...
- 强制另存文件和加扩展名的代码c#
强制另存为文件+扩展名的代码using System;using System.Collections.Generic;using System.Linq;using System.Web; name ...
- PacBio下机数据如何看?
一开始拿到三代测序的下机数据时,蒙了,readme ?三代测序的下机数据都有哪些,以及他们具体的格式是怎么样的(以sequel 平台为主). 测序过程 SMRTbell A adapter通用接头,两 ...
- R 语言安装问题集锦
R安装问题R CMD INSTALL -l /data1/jhh/envirment/R/R_lib /data1/jhh/software/qvalue_2.10.0.tar.gz 问题1 :con ...