2020-12-19

逆变(contravariant)与协变(covariant)是C#4新增的概念,许多书籍和博客都有讲解,我觉得都没有把它们讲清楚,搞明白了它们,可以更准确地去定义泛型委托和接口,这里我尝试画图详细解析逆变与协变。

变的概念

我们都知道.Net里或者说在OO的世界里,可以安全地把子类的引用赋给父类引用,例如:

//父类 = 子类
string str = "string";
object obj = str;//变了

C#里又有泛型的概念,泛型是对类型系统的进一步抽象,比上面简单的类型高级,把上面的变化体现在泛型的参数上就是我们所说的逆变与协变的概念。通过在泛型参数上使用in或out关键字,可以得到逆变或协变的能力。下面是一些对比的例子:

协变(Foo<父类> = Foo<子类> ):

//泛型委托:
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<父类>

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泛型参数用作方法返回值的泛型接口时:

public interface IPlayB<in T>
{T Test();
}

出现了如下编译错误:

错误    1    方差无效: 类型参数“T”必须为“CovarianceAndContravariance.IPlayB<T>.Test()”上有效的 协变式。“T”为 逆变。  

到这里,我们大致知道了逆变与协变的相关概念,那么为什么把泛型参数限制为in或者out就可以“变”呢?下面尝试画图解释原理。

协变不是理所当然的,逆变也没有“逆”

我们先来看看不支持逆变与协变的泛型,把子类赋给父类,再执行父类方法的具体流程,对于这样一个简单的例子的Test方法:

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("");

它实际的流程是这样的:
image

即调用父类的方法,其实实际是调用子类的方法。可以看到,这个方法能够安全的调用,需要两个条件:
1.变式(父)的方法参数能安全转为原式(子)的 参数;
2.原式(子)的返回值能安全的转为变式的返回值。不幸的是参数的流向跟返回值的流向是相反的,所以对于既是in,又是out的泛型参数来说,肯定 是行不通的,其中一个方向必然不能安全转换的。例如,对上面的例子,我们尝试“变”:

Base<object> BaseObject = null;
Base<string> BaseString = null;
BaseObject = BaseString;//编译失败
BaseObject.Test("");

这里的“实际流程”如下,可以看到,参数那里是object是不能安全转换为string,所以编译失败:
image

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

image

可以看到,从”实际流程图”来看,逆变根本没有“逆”,都离不开只能安全地把子类的引用赋给父类引用这个根本。

来到这里应该基本理解逆变与协变了,不过装配脑袋的这篇文章有个更高级的问题,原文也有解答,这里我用上面画图的方式去理解它。

图解逆变与协变的相互作用

问题的提出,你知道那个正确吗?

public interface IBar<in T> { }
//应该是in
public interface IFoo<in T>
{void Test(IBar<T> bar);
}
//还是out
public interface IFoo<out T>
{void Test(IBar<T> bar);
}

答案是,如果是in的话,会编译失败,out才正确(当然不要泛型修饰符也能通过编译,但IFoo就没有协变能力了)。这里的意思就是说,一个有协变(逆变)能力的泛型(IBar),作为另一个泛型(IFoo)的参数时,影响到了它(IFoo)的泛型的定义。乍一看以为是in的其中一个陷阱是T是在 Test方法的参数里的,所以以为是in。但这里Test的参数根本不是T,而是IBar<T>

我们画个图来理解它。既然out可以通过,那么它的“协变流程图”应该如下:

image

图跟前面那些大致一样,但理解它要跟问题相反(上面问题是先定义好IBar,再去定义IFoo)。
1.我们定义好一个有协变能力的IFoo,这是前提。
2.可以推出,上面的流程是成立的。
3.这个流程重点是参数流向,要使整个流程成立,就必须使IBar<string> = IBar<object>成立,这不就是逆变吗?整个结论就是,有协变能力的IFoo要求它的泛型参数(IBar)有逆变能力。其实根据上面的箭头也可以理解,因为原式和变式的变向跟参数的变向是相反的,导致了它们要有相反的能力,这就是装配脑袋文章说的:方法参数的协变-反变互换原则。根据这个原理,也很容易得出,如果Test方法的返回值是IBar<T>,而不是参数,那么就要求IBar<T>要有协变能力,因为返回值的箭头与原式和变式的变向的箭头是同向的。

The End!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/436995.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【数学】定积分和不定积分的区别

不定积分的研究对象是某函数下的映射关系积分后&#xff0c;得到怎样的新的映射关系。 定积分的研究对象是积分的值。

微软模拟飞行10厦门航空涂装_《微软飞行模拟器》多人游戏模式演示:可组队飞行...

IT之家3月29日消息 《微软飞行模拟器》(Flight Simulator)是2020年最受期待的游戏之一&#xff0c;开发商Asobo Studio现在分享了一段新的视频&#xff0c;详细介绍了《微软飞行模拟器》的多人游戏模式。《微软飞行模拟器》游戏的主模式将看到所有玩家在同一个世界中一起玩。微…

【信号与系统】系统特性

系统 系统就是一个包含f(t)、y(t)及其衍生函数&#xff08;如导数&#xff0c;复合函数&#xff09;的方程。 所以在你看到一个方程的时候&#xff0c;第一反应是&#xff1a; 方程描述了一个系统&#xff0c;在时刻t&#xff0c;可以把f(t)变成y(t)。如何变的呢&#xff1f;由…

【转】UDP协议格式以及在java中的使用

UDP协议格式以及在java中的使用 UDP是面向无连接的通讯协议&#xff0c;由于通讯不需要连接&#xff0c;所以可以实现广播发送。UDP通讯时不需要接收方确认&#xff0c;属于不可靠的传输&#xff0c;可能会出现丢包现象&#xff0c;实际应用中要求程序员编程验证。 UDP适用于…

python网页填表教程_PythonSpot 中文系列教程 · 翻译完成

原文&#xff1a;PythonSpot Python Tutorials 协议&#xff1a;CC BY-NC-SA 4.0 欢迎任何人参与和完善&#xff1a;一个人可以走的很快&#xff0c;但是一群人却可以走的更远。在线阅读ApacheCN 学习资源目录PythonSpot 中文系列教程初学者 介绍Python 字符串字符串&#xff0…

Qt生成的exe中为什么会带有不该有的盾牌?

参考 为什么EXE文件出现了不该出现的“盾牌”什么情况下 exe 会自动加上 UAC 的盾标&#xff1f;

【转】TCP/IP协议到底在讲什么?【乐搏TestPro】

用比喻和漫画给有需要的小伙伴解释下IP、TCP捎带题一下各种协议与HTTP协议的关系&#xff1b; 目录&#xff1a; 负责传输的IP协议 确保可靠性的TCP协议 各种协议与HTTP协议的关系 一、负责传输的IP协议 按照层次分&#xff0c;IP&#xff08;全称&#xff1a;Internet Prot…

python文本去重函数_python3.4.3下逐行读入txt文本并去重的方法

读写文件时应注意的问题包括&#xff1a;1.字符编码2.操作完成即时关闭文件描述符3.代码兼容性几种方法&#xff1a;#!/bin/python3original_list1[" "]original_list2[" "]original_list3[" "]original_list4[" "]newlist1[" &q…

Qt中标绘功能的实现方法对比

使用Qt开发桌面程序&#xff0c;经常会有标绘的需求&#xff0c;一般有以下几点&#xff1a; 新建&#xff1a;圆、矩形、椭圆、文字标注&#xff0c;插入图像等&#xff1b;编辑&#xff1a;指对已标绘内容的属性编辑修改功能&#xff1b;删除&#xff1a;指对已标绘内容的删…

【转】PE文件结构详解--(完整版)

&#xff08;一&#xff09;基本概念 PE&#xff08;Portable Execute&#xff09;文件是Windows下可执行文件的总称&#xff0c;常见的有DLL&#xff0c;EXE&#xff0c;OCX&#xff0c;SYS等&#xff0c;事实上&#xff0c;一个文件是否是PE文件与其扩展名无关&#xff0c;P…

sap 订单状态修改时间_SAP中对于获取订单的状态

在SAP中对于如何获取订单的状态&#xff0c;提供了至少两个函数&#xff0c;分别是 STATUS_READ 和 STATUS_TEXT_EDIT。下面简单介绍这两个函数1.STATUS_READ 改函数的实现原理大概是通过订单的对象好(OR订单号) 到JEST中取出字段STAT INACT.JEST表中STAT是一串从字面看不出…

【转】%~dp0是什么意思

转载自 www.cnblogs.com/yxsylyh 转载内容如下&#xff1a; cd /D %~dp0的意思如下&#xff1a; 更改当前目录为批处理本身的目录 比如你有个批处理a.bat在D:\qq文件夹下 a.bat内容为 cd /d %~dp0 在这里 cd /d %~dp0的意思就是cd /d d:\qq %0代表批处理本身 d:\qq\a.b…

AutoCode For XML(XML解析代码生成器)发布

项目地址 AutoCode For XML on Gitee bug反馈、意见建议 bug反馈、意见建议请直接在此项目主页上进行&#xff01; 版本更新 AutoCode For XML v1.0.0发布啦&#xff01; 第一个发行版本&#xff0c;主要用于测试。 下载地址&#xff1a;点我 本工具由Qt未来工程师原创发布。…

wince投屏苹果手机_怎么把手机上的导航映射到中控屏

展开全部第一种&#xff1a;通过MHL线进行手机屏幕和车载屏幕连62616964757a686964616fe78988e69d8331333431353366接实现这种连接方式&#xff0c;必须满足三个条件&#xff1a;一是手机需要支持MHL功能&#xff0c;目前大多数安卓智能手机均具备这一功能&#xff1b;二是车上…

【转】逆变与协变详解

逆变&#xff08;contravariant&#xff09;与协变&#xff08;covariant&#xff09;是C#4新增的概念&#xff0c;许多书籍和博客都有讲解&#xff0c;我觉得都没有把它们讲清楚&#xff0c;搞明白了它们&#xff0c;可以更准确地去定义泛型委托和接口&#xff0c;这里我尝试画…

设计模式(二)设计模式的本质

简介 设计模式是计算机前辈们&#xff0c;总结项目开发成败经验&#xff0c;得出的一套最佳实践理论。它并不是高高在上、不切实际的理论&#xff0c;而是具体到代码编写层面的指导理论。 从学习编写代码开始&#xff0c;我们就被教导&#xff0c;要写高内聚、低耦合、可复用…

angular设置referer_Angular-cli 构建应用的一些配置

Angular-cli 构建应用的一些配置标签(空格分隔)&#xff1a; Angular直接使用 ng build --prod --build-optimizer --base-href/ 来发布base-href可以设置服务器上的某个子路径&#xff0c;使用 ng build --base-href/my/path/如果打包静态文件(js和css)不放在和index.html同一…

设计模式(三)创建型模式

前言 根据菜鸟教程的目录&#xff0c;我们首先来看看创建型模式。 创建型模式研究&#xff1a; 实际应用中通常有哪些不同的创建对象的场景&#xff1b;在不同的场景下&#xff0c;如何更好地编写创建对象的代码。主要研究构造函数。 下面分别对创建型模式下的各种具体模式进…

CSDN改版,找不到各种入口,链接放下面

https://mp.csdn.net/console/article?spm1010.2135.3001.5128 https://mp.csdn.net/console/column/allColumnList 分类管理 https://mp.csdn.net/console/article 文章管理 https://mp.csdn.net/console/upDetailed 资源管理 https://mp.csdn.net/editor/html?spm1011.…

python中pca算法_python实现PCA算法01

python实现PCA算法Software version&#xff1a; Python 2.7.12 |Anaconda 4.2.0 (64-bit)|法1. 编程一步一步实现法2. sklearn我们以定义函数的形式来一步一步进行1.1 导入模块&#xff1a;Numpy&#xff0c;Pandas# -*- coding: utf-8 -*-# Time : 2017/8/17 14:20# Author :…