C#协变逆变
文章目录
- 协变
- 协变接口的实现
- 逆变
- 里氏替换原则
协变
协变概念令人费解,多半是取名或者翻译的锅,其实是很容易理解的。
比如大街上有一只狗,我说大家快看,这有一只动物!这个非常自然,虽然动物并不严格等于狗,但不会有人觉得我说的不对,把狗变成动物就是协变,C#也支持这个:
// C#6顶级语句
Dog dog= new Dog();
Animal animal= dog;interface Animal
{}class Dog : Animal
{}
那么接下来,大街上有一群狗,我说有一群动物,按理说也是对的,但看样子C#不这么认为
List<Dog> dogLst = new List<Dog>();
List<Animal> aniLst = dogLst; //飙红飙红飙红了
interface Animal {}
class Dog : Animal {}
原因其实很容易理解,毕竟在上述的代码中,写了Dog:Animal
,即声明了狗是动物的子类,但是并没有写List<Animal> : List<Dog>
,换言之,从来没有声明过一群狗是一群动物的子类。
但是,如果不用List
,而用其父类IEnumerable
,写成下面这样,就又不报错了。
List<Dog> dogLst = new List<Dog>();
IEnumerable<Animal> aniLst = dogLst;
换言之,C#承认List<Dog>
是IEnumerable<Animal>
的子类,个中差别,只需一览源码,就会知晓:
public interface IEnumerable<out T> : IEnumerable
public class List<T> : ..., IEnumerable<T>, ...
IEnumerable
无非比List
多了一个out
参数,有了这个参数,就拥有了协变的功能,从而当U
是T
的子类时,可以支持IEnumerable<U>
到IEnumerable<T>
的转换。
在官方文档中,指明了具有out
关键字的泛型接口包括IEnumerable<T>
, IEnumerator<T>
, IQueryable<T>
和IGrouping<TKey,TElement>
。
协变接口的实现
协变和逆变目前只能在泛型接口和委托中使用,下面新建一个泛型接口,并使用关键字out
。由于使用.Net6.0的顶级语句,所以接口和类的声明放在后面。
IOut<string> outStr = new Out();
IOut<object> outObj = outStr;
Console.WriteLine(outObj.getName());interface IOut<out T>
{T getName();
}class Out : IOut<string>
{public string getName(){return GetType().Name;}
}
编译运行,最后输出Out
,即outObj
尽管在声明的时候用的是IOut<object>
,但在IOut
的out
修饰符的作用下,成功让IOut<object>
变成了IOut<string>
的父类,得以顺利调用Out
中的方法。
那么接下来,如果想让getName
更加完备一些,例如要求实现getName(T name)
这样的功能,那么经out
修饰的协变接口就无能为力了,像下面这样的写法果然被无情地飙红了
interface IOut<out T>
{void getName(T name);
}
逆变
VS作为宇宙顶级IDE,协变逆变十分拎得清,上述代码在飙红的同时,直接给出如下错误
变型无效: 类型参数“T”必须是在“IOut.getName(T)”上有效的 逆变式。“T”为 协变。
换言之,如果想让泛型接口可以输入泛型参数,那么需要用到逆变,具体写法如下,其中修饰符in
表示逆变
IIn<object> inObj = new In();
IIn<string> inStr = inObj;
inStr.getName("in");interface IIn<in T>
{void getName(T name);
}class In : IIn<object>
{public void getName(object name){Console.WriteLine(name);}
}
逆变和协变最大的不同,并非in
和out
这两个修饰符的字数,而是整个替换逻辑发生了变化,上述代码中,实际上是作为子类的string
调用了通过父类object
作为参数定义的函数。
里氏替换原则
在具体实现了协变与逆变之后,总觉得那里怪怪的,最怪的其实还是下面这行代码的错误
错错错错错错错错错错错错错错错错错错错错错错错错错错
interface IOut<out T>
{void getName(T name);
}
而且可以想象,与之相对应的下面的逆变代码也是不对的
错错错错错错错错错错错错错错错错错错错错错错错错错错
interface IOut<in T>
{T getName();
}
接下来复盘一下产生这种现象的原因,为了破除命名带来的困扰,接下来考虑泛型接口I<T>
,其中有一个函数T test(T t)
。现有两个特定的继承自泛型接口I<A>
和I<B>
的类,假设I<A>
要调用I<B>
中的方法,那么其流程如下
A I<A>.test(A t)
,即输入一个A类型的参数- 将这个
A
类型的参数t
,传入到B I<B>.test(B t)
。由于I<B>
要求输入B
类型的参数,所以要求A
可以转换为B
类型。 B I<B>.test(B t)
计算完毕,返回一个B
类型的参数- 这个
B
类型的参数又被返回给最初的调用者A I<A>.test
,而这时I<A>
的函数最终将返回一个A
类型的参数,换言之,在这个步骤,要求B
可以转换为A
。
A
能转为B
,然后还得B
能转为A
,同时A
和B
还不相等,这显然是不可能的。
所以逆变和协变分别实现了第2步和第4步。
如果I<A>
想要调用I<B>test(B t)
中的函数,那么A
类型必须可以转成B
类型。正如string
可以转为object
一样,此即逆变,用in
修饰,其作用场合为子类调用父类中的方法。
如果I<A>
想要调用B I<B>test()
,那么作为返回值的B
类型必须可以转化为A
类型,此即协变,用out
修饰,正是父类调用子类的方法。
协变和逆变的统一之处在于,二者都严格遵循这子类可以转变为父类的规则,此即里氏替换。这是1987年,芭芭拉·利斯科夫提出的,她也是2008年图灵奖得主。
在协变逆变的过程中,对里氏替换的遵循主要表现在当子类方法重载父类方法时
- 方法的输入参数要更加宽松,此即逆变(
IOut<object>
调用IOut<string>
,object
比string
更宽松) - 方法的返回值要更加严格,此即协变(
IOut<string>
调用IOut<object>
,string
比object
更严格)
相关文章:

C#协变逆变
文章目录协变协变接口的实现逆变里氏替换原则协变 协变概念令人费解,多半是取名或者翻译的锅,其实是很容易理解的。 比如大街上有一只狗,我说大家快看,这有一只动物!这个非常自然,虽然动物并不严格等于狗…...

算法设计与分析期末考试复习(四)
贪心算法(Greedy Algorithm) 找零钱问题 假设有4种硬币,面值分别为:二角五分、一角、五分和一分,现在要找给顾客六角三分钱,如何找使得给出的硬币个数最少? 首先选出1个面值不超过六角三分的最…...

qsort函数排序数据 and 模拟实现qosrt函数(详解)
前言:内容包括使用库函数qsort排序任意类型的数据,模拟实现qsort函数(冒泡排序的逻辑) 我们先了解qsort函数的语法:qsort函数默认按照升序排序数据 void qsort (void* base, size_t num, size_t size,int (*compar)(…...

Mysql视图,存储过程,触发器,函数以及Mysql架构
一,视图视图是基于查询的一个虚拟表 , 也就是将sql语句封装起来, 要用的时候直接调用视图即可, select语句查询的表称为基表, 查询的结果集称为虚拟表, 基本表数据发生了改变, 那么视图也会发生改变, 使用视图就是为了简化查询语句.1.CREATE VIEW view_admin AS SELECT * FROM…...

什么是线程死锁?如何解决死锁问题
死锁,一组互相竞争的资源的线程之间相互等待,导致永久阻塞的现象。 如下图所示: 与死锁对应的,还有活锁,是指线程没有出现阻塞,但是无限循环。 有一个经典的银行转账例子如下: 我们有个账户类…...

C语言几种判断语句简述
C 判断 判断结构要求程序员指定一个或多个要评估或测试的条件,以及条件为真时要执行的语句(必需的)和条件为假时要执行的语句(可选的)。 C 语言把任何非零和非空的值假定为 true,把零或 null 假定为 fals…...

【python学习笔记】:SQL常用脚本(二)
11、四舍五入ROUND函数 ROUND ( numeric_expression , length [ ,function ] ) function 必须为 tinyint、smallint 或 int。 如果省略 function 或其值为 0(默认值),则将舍入 numeric_expression。 如果指定了0以外的值,则将截…...

【Linux】进程地址空间
文章目录🎪 进程地址空间🚀1.写时拷贝与虚拟地址🚀2.地址空间引入🚀3.地址空间的意义⭐3.1 虚拟地址寻址⭐3.2 虚拟地址意义🎪 进程地址空间 地址空间(address space)表示任何一个计算机实体所…...

Qt音视频开发17-vlc内核回调拿图片进行绘制
一、前言 在众多播放器中,支持的种类格式众多,并支持DVD影音光盘,VCD影音光盘及各类流式协议,提供了sdk进行开发,这点是至关重要的,尽管很多优秀的播放器很牛逼,由于没有提供sdk第三方开发&…...

安装配置DHCP
本次实验采用CentOS71.检查在安装DHCP之前先使用rpm命令查看系统中已有的DHCP软件包rpm -qa | grep dhcp由此可知,系统中尚未安装DHCP软件包2.安装我们可以使用yum命令为系统安装DHCP软件包yum -y install dhcp安装完成后再次检查可以看到DHCP软件包3.配置dhcp配置文…...

MarkDown中写UML图的方法
目录序UML图之顺序图顺序图的四个要素关于消息箭头的语法Mermaid中顺序图的简单例子样例用小人表示对象为对象设置别名激活对象UML图之类图类图中常见的关系关于不同类型关系的语法Mermaid中类图的简单例子样例类定义的两种方式为类定义成员双向关系的表示多重性关系的表示UML之…...

Axure8设计—动态仪表盘
本次分享的的案例是Axure8制作的动态仪表盘,根据设置的数值,仪表盘指针旋转到相应的值位置 预览地址:https://2qiuwg.axshare.com 下载地址:https://download.csdn.net/download/weixin_43516258/87502161 一、制作原型 1、首先创建空白页…...

【C++】类和对象的六个默认成员函数
类的6个默认成员函数构造函数概念特性析构函数概念特性拷贝构造函数概念特征拷贝构造函数典型调用场景:赋值运算符重载运算符重载赋值运算符重载取地址及const取地址操作符重载类的6个默认成员函数 到底什么是类的6个默认成员函数呢?相信大家一定对此怀…...

4、算法MATLAB---认识矩阵
认识矩阵1、矩阵定义和基本运算1.1 赋值运算符:1.2 等号运算符:1.3 空矩阵1.4 一行一列矩阵1.5 行矩阵(元素用空格或逗号分隔)1.6 列矩阵(分号表示换行)1.7 m行n列的矩阵:行值用逗号间隔&#x…...

vue3+rust个人博客建站日记2-确定需求
反思 有人说过我们正在临近代码的终结点。很快,代码就会自动产生出来,不需要再人工编写。程序员完全没用了,因为商务人士可以从规约直接生成程序。 扯淡!我们永远抛不掉代码,因为代码呈现了需求的细节。在某些层面上&a…...

Linux安装云原生网关Kong/KongA
目录1 概述2 创建服务器3 安装postgres4 安装kong5 安装node6 安装KONGA1 概述 Kong Kong是一款基于OpenResty(NginxLua模块)编写的高可用、易扩展的开源API网关,专为云原生和云混合架构而建,并针对微服务和分布式架构进行了特别…...

Vue学习笔记(2)
2.1 事件处理 2.1.1 事件监听器 JavaScript:通过获取DOM对象再往DOM对象上使用addEventListener注册监听事件 const btn document.querySelector(#my-button) btn.addEventListener(click, function() {alert(点击事件!) })jQuery:通过$选择器绑定对象…...

2023年三月份图形化四级打卡试题
活动时间 从2023年3月1日至3月21日,每天一道编程题。 本次打卡的规则如下: 小朋友每天利用10~15分钟做一道编程题,遇到问题就来群内讨论,我来给大家答疑。 小朋友做完题目后,截图到朋友圈打卡并把打卡的截图发到活动群…...

Python操作Excel
Python中对Excel文件的操作包括:读、写、修改。如果要对其进行如上的操作需要导入Python的第三方模块:xlrd、xlwd、xlutils,其分别对应Python的读、写、修改的操作 一、安装Python的第三方模块 二、操作Excel的基本步骤 1、导入响对应的模…...

Codeforces Round #853 (Div. 2) C. Serval and Toxel‘s Arrays【统计次数,算贡献】
链接 传送门 分析 这道题想法其实很简单,样例的计算方法一定要看懂。以样例1为例,根据他的操作方法可以得到两个新的数组,和一个原来的数组,总共三个数组。 1 2 3 4 2 3 4 5 3 他们两两配对去重,求出总的value。由于每…...

微信小程序-1:比较两数的大小
程序来源》微信小程序开发教程(第二章) 主编:黄寿孟、易芳、陶延涛 ISBN: 9787566720788 程序运行结果: <!--index.wxml--> <view class"container"> <text>第一个数字:&…...

数据结构——树
深度优先/广度优先遍历深度优先:访问根节点对根节点的 children 挨个进行深度优先遍历const tree {val: "a",children: [{val: "b",children: [{val: "d",children: [],},{val: "e",children: [],},],},{val: "c&quo…...

【华为OD机试模拟题】用 C++ 实现 - 找到它(2023.Q1)
最近更新的博客 【华为OD机试模拟题】用 C++ 实现 - 去重求和(2023.Q1) 文章目录 最近更新的博客使用说明找到它题目输入输出示例一输入输出示例二输入输出说明Code使用说明 参加华为od机试,一定要注意不要完全背诵代码,需要理解之后模仿写出,通过率才会高。 华为 OD …...

python中yield的使用
在 Python 中,yield 是一个关键字,它用于定义生成器函数。生成器函数是一个特殊的函数,可以返回一个迭代器,当生成器函数被调用时,它不会立即执行,而是返回一个生成器对象,通过迭代生成器对象可…...

GO进阶(4) 深入Go的内存管理
Go语言成为高生产力语言的原因之一自己管理内存:Go抛弃了C/C中的开发者管理内存的方式,实现了主动申请与主动释放管理,增加了逃逸分析和GC,将开发者从内存管理中释放出来,让开发者有更多的精力去关注软件设计ÿ…...

【C++】类与对象理解和学习(下)
放在专栏【C知识总结】,会持续更新,期待支持🌹建议先看完【C】类与对象理解和学习(上)【C】类与对象理解和学习(中)本章知识点概括Ⅰ本章知识点概括Ⅱ初始化列表前言在上一篇文章中,…...

【Neo4j】Spring Data Neo4j APi阅读随笔
引言 关于Spring boot整合Neo4j的官方api翻译&学习随笔 (TOC) 一、准备工作 1.注入依赖 <dependency><groupId>org.springframework.data</groupId><artifactId>spring-data-jpa</artifactId></dependency>2.配置yml文件 这里是本…...

JVM内存模型简介
1 程序计数器 程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完。 ja…...

k8s如何给node添加标签
一、为什么需要标签? k8s集群如果由大量节点组成,可将节点打上对应的标签,然后通过标签进行筛选及查看,更好的进行资源对象的相关选择与匹配 二、怎么查看目前node上具有的标签 [rootmaster01 ~]# kubectl get node --show-labels NAME …...

【大数据Hive】Hive ddl语法使用详解
一、前言 使用过关系型数据库mysql的同学对mysql的ddl语法应该不陌生,使用ddl语言来创建数据库中的表、索引、视图、存储过程、触发器等,hive中也提供了类似ddl的语法。本篇将详细讲述hive中ddl的使用。 二、hive - ddl 整体概述 在Hive中,DA…...