C#精粹--协变和逆变
概念
协变和逆变来源于类型和类型之间的绑定,C#4.0开始在泛型的接口和委托上支持协变和逆变,不过在这个版本之前的委托也是支持协变和逆变的。比如数组就支持协变,但是这不是一个好的特性,这C#初期版本从java引入的一个特性,当时的设计者认为C#应该尽可能的像java的一些特性靠拢,因为java当时太火了。
如果有类型Parent和其子类Sub,那么Parent p=new Sub();这种的类型转换是安全的。如果有一种类型和Parent类型进行了绑定,比如说Parent[]数组,如果Sub[]到Parent[]的转换是安全的,我们就说是协变,如果相反的方向上转换是安全的,我们就说是逆变了。
C#的协变和逆变没有完整的阐述这个数学和物理上的概念,C#为协变和逆变的实现捆绑了很多条件:
①必须是泛型的委托或接口
②协变只能支持返回类型
③逆变只能支持参数
而这些条件总归是实现了一个面向对象原则:里氏替换原则。也就是说,不管是协变还是逆变,最终都是一种类型安全的转换,没有不安全的转换发生。
有了上面这些条件的约束还不够,C#还明确的指定了两个关键字来让开发人员更加明确的知道声明的泛型类型参数到底用在了哪里:给协变对应的关键字是out,逆变对应的关键字是in,看一下示例:
IEnumerable<out T>//IEnumerable这个接口对T协变
Action<int TParameter>//Action这个委托对TParameter逆变
上面这个解释基本阐明了泛型的协变和逆变的概念,下面来看一些例子
示例
委托的协变和逆变
public delegate object Test(string obj);
class Program
{ static void Main(string[] args)
{
Test test = TestMethod;
var result = test("hello world");
Console.ReadKey(); string TestMethod(object str)
{
Console.WriteLine(str);
return "hello world";
}
}
}
代码第一行定义了一个返回object,输入string的名为Test的委托。我们在main方法中定义了一个本地方法(C#7的功能)然后用这个方法来实例化一个Test委托(利用方法组转换特性),可以看到这个Test委托既表达了协变的意思(返回类型的协变:object=string,由一个object类型引用一个返回string类型的方法肯定没有问题),又表达了逆变的意思(参数类型的逆变:实际上还是object=string,为什么这么说呢?因为真正要执行的方法是TestMethod,这个方法需要一个object类型的参数,但是这个方法的地址指向了一个Test委托,这个委托代表了那种输入类型为string,输出类型为object的方法,当我们调用Test(...)这个委托时,很明显需要传入一个string类型的变量进去,但是真正执行的方法是TestMethod,而这个TestMethod需要传入一个object类型的参数,那么,这个string类型传入这个TestMethod方法内部肯定是没有问题的,同时,也说明了,逆变实际上还是一种安全转换,按这个例子来说(参数要求是object的,实际传入了一个string),还是从string类型转换成了object类型了嘛!)。
上面解释有点儿啰嗦,但也基本说明了原理了。
协变
接口的协变最著名的例子就是IEnumerable<T>:
IEnumerable<object> ss = new List<string>();
逆变
逆变的例子是一个Action<T>:
public static void Main(string[] args)
{
Action<string> action = Test;
void Test(object obj)
{
Console.WriteLine(obj);
}
}
这些关于协变和逆变的解释在上面通通都有,就不再解释了。
协变和逆变的相互作用
这是一个相当有趣的话题,我们先来看一个例子:
interface IFoo<in T>
{
}
interface IBar<in T>
{
void Test(IFoo<T> foo); //对吗?
}
你能看出上述代码有什么问题吗?我声明了in T,然后将他用于方法的参数了,一切正常。但出乎你意料的是,这段代码是无法编译通过的!反而是这样的代码通过了编译:
interface IFoo<out T>
{
}
interface IBar<in T>
{
void Test(IFoo<T> foo); //对吗?
}
什么?明明是out参数,我们却要将其用于方法的参数才合法?初看起来的确会有一些惊奇。我们需要费一些周折来理解这个问题。现在我们考虑IBar<string>,它应该能够协变成IBar<object>,因为string是object的子类。因此IBar.Test(IFoo<string>)也就协变成了IBar.Test(IFoo<object>)。当我们调用这个协变后方法时,将会传入一个IFoo<object>作为参数。想一想,这个方法是从IBar.Test(IFoo<string>)协变来的,所以参数IFoo<object>必须能够变成IFoo<string>才能满足原函数的需要。这里对IFoo<object>的要求是它能够反变成IFoo<string>!而不是协变。也就是说,如果一个接口需要对T协变,那么这个接口所有方法的参数类型必须支持对T的反变。同理我们也可以看出,如果接口要支持对T反变,那么接口中方法的参数类型都必须支持对T协变才行。这就是方法参数的协变-反变互换原则。所以,我们并不能简单地说out参数只能用于返回值,它确实只能直接用于声明返回值类型,但是只要一个支持反变的类型协助,out类型参数就也可以用于参数类型!换句话说,in参数除了直接声明方法参数之外,也仅能借助支持协变的类型才能用于方法参数,仅支持对T反变的类型作为方法参数也是不允许的。要想深刻理解这一概念,第一次看可能会有点绕,建议有条件的情况下多进行一些实验。
刚才提到了方法参数上协变和反变的相互影响。那么方法的返回值会不会有同样的问题呢?我们看如下代码:
interface IFooCo<out T>
{ }
interface IFooContra<in T>
{ }
interface IBar<out T1, in T2>
{
IFooCo<T1> Test1(); IFooContra<T2> Test2();
}
我们看到和刚刚正好相反,如果一个接口需要对T进行协变或反变,那么这个接口所有方法的返回值类型必须支持对T同样方向的协变或反变。这就是方法返回值的协变-反变一致原则。也就是说,即使in参数也可以用于方法的返回值类型,只要借助一个可以反变的类型作为桥梁即可。如果对这个过程还不是特别清楚,建议也是写一些代码来进行实验。至此我们发现协变和反变有许多有趣的特性,以至于在代码里in和out都不像他们字面意思那么好理解。当你看到in参数出现在返回值类型,out参数出现在参数类型时,千万别晕倒,用本文的知识即可破解其中奥妙。
文章部分引用自:https://www.cnblogs.com/Ninputer/archive/2008/11/22/generic_covariant.html
C#精粹--协变和逆变的更多相关文章
- C#4.0泛型的协变,逆变深入剖析
C#4.0中有一个新特性:协变与逆变.可能很多人在开发过程中不常用到,但是深入的了解他们,肯定是有好处的. 协变和逆变体现在泛型的接口和委托上面,也就是对泛型参数的声明,可以声明为协变,或者逆变.什么 ...
- C#协变和逆变
我们知道在C#中,是可以将派生类的实例赋值给基类对象的.
- C# 泛型的协变和逆变
1. 可变性的类型:协变性和逆变性 可变性是以一种类型安全的方式,将一个对象当做另一个对象来使用.如果不能将一个类型替换为另一个类型,那么这个类型就称之为:不变量.协变和逆变是两个相互对立的概念: 如 ...
- 不变(Invariant), 协变(Covarinat), 逆变(Contravariant) : 一个程序猿进化的故事
阿袁工作的第1天: 不变(Invariant), 协变(Covarinat), 逆变(Contravariant)的初次约 阿袁,早!开始工作吧. 阿袁在笔记上写下今天工作清单: 实现一个scala类 ...
- 再谈对协变和逆变的理解(Updated)
去年写过一篇博客谈了下我自己对协变和逆变的理解,现在回头看发现当时还是太过“肤浅”,根本没理解.不久前还写过一篇“黑”Java泛型的博客,猛一回头又是“肤浅”,今天学习Java泛型的时候又看到了协变和 ...
- 【转】c# 协变和逆变
本文转自:http://www.cnblogs.com/rr163/p/4047404.html C#的协变和逆变 由子类向父类方向转变是协变,用out关键字标识,由父类向子类方向转变是逆变,用in关 ...
- .NET 4.0中的泛型的协变和逆变
转自:http://www.cnblogs.com/jingzhongliumei/archive/2012/07/02/2573149.html 先做点准备工作,定义两个类:Animal类和其子类D ...
- 深入理解 C# 协变和逆变
msdn 解释如下: “协变”是指能够使用与原始指定的派生类型相比,派生程度更大的类型. “逆变”则是指能够使用派生程度更小的类型. 解释的很正确,大致就是这样,不过不够直白. 直白的理解: “协变” ...
- Java用通配符 获得泛型的协变和逆变
Java对应泛型的协变和逆变
随机推荐
- Beta冲刺(5/5)
队名:無駄無駄 组长博客 作业博客 组员情况 张越洋 过去两天完成了哪些任务 验收了小程序的主要功能 制作Beta展示所需要用到的视频 制作Beta展示PPT 准备Beta答辩 提交记录(全组共用) ...
- 怎么样使element ui 的table某列变色
第一步.在el-table里面加上:row-style="rowClass" <el-table :data="targetCarList" border ...
- Ecms7.5版CK编辑器保留word格式如何修改
7.5版的编辑器默认会清除多余的word代码,如果要保留word格式怎么修改? CKeditor编辑器默认复制会清除多余word代码,如果要保留word格式可以按下面修改配置: 修改 /e/admin ...
- Spring Boot 支持 HTTPS 如此简单,So easy!
这里讲的是 Spring Boot 内嵌式 Server 打 jar 包运行的方式,打 WAR 包部署的就不存在要 Spring Boot 支持 HTTPS 了,需要去外部对应的 Server 配置. ...
- ubuntu之路——day17.1 卷积操作的意义、边缘检测的示例、filter与padding的关系、卷积步长
感谢吴恩达老师的公开课,以下图片均来自于吴恩达老师的公开课课件 为什么要进行卷积操作? 我们通过前几天的实验已经做了64*64大小的猫图片的识别. 在普通的神经网络上我们在输入层上输入的数据X的维数为 ...
- typescript - 5.接口
接口的作用: 在面向对象的编程中,接口是一种规范的定义,它定义了行为和动作的规范,在程序设计里面,接口起到一种限制和规范的作用.接口定义了某一批类所需要遵守的规范,接口不关心这些类的内部状态数据,也不 ...
- RAID技术全解图解-RAID0、RAID1、RAID5、RAID100【转】
图文并茂 RAID 技术全解 – RAID0.RAID1.RAID5.RAID100…… RAID 技术相信大家都有接触过,尤其是服务器运维人员,RAID 概念很多,有时候会概念混淆.这篇文章为网络转 ...
- EasyNVR摄像机网页H5全平台无插件直播流媒体播放服务二次开发之接口鉴权示例讲解
背景需求 EasyNVR的使用者应该都清楚的了解到,EasyNVR一个强大的功能就是可以进行全平台的无插件直播.主要原因在于rtsp协议的视频流(默认是需要插件才可以播放的)经由EasyNVR处理可以 ...
- [LeetCode] 67. Add Binary 二进制数相加
Given two binary strings, return their sum (also a binary string). The input strings are both non-em ...
- 玩转CONSUL(3)–大规模部署的性能开销定量分析
1. 引言 今天有朋友问萌叔,consul能否在大规模生产环境下进行应用.场景是总计大约10w+台机器,分为3 ~ 4个机房,单个机房最多3w万+机器.这个问题大的,可把萌叔吓了跳,部门里面consu ...