C#泛型的逆变协变之个人理解
作者:崩坏的领航员 发布时间:2021-05-28 16:33:03
一般来说, 泛型的作用就类似一个占位符, 或者说是一个参数, 可以让我们把类型像参数一样进行传递, 尽可能地复用代码。
我有个朋友, 在使用的过程中发现一个问题
IFace<object> item = new Face<string>(); // CS0266
public interface IFace<T>
{
string Print(T input);
}
public class Face<T> : IFace<T>
{
public string Print(T input) => input.ToString();
}
Q:   string
明明是 object
的子类, 为啥这样赋值会报错呢???
A:   因为 Face<string>
实现的是 IFace<string>
, 而 IFace<string>
并不是 IFace<object>
的子类
Q:   但是 string
是 object
的子类啊, IFace<string>
可不就是 IFace<object>
吗?
A:   如果只论接口定义, 看起来确实是这样的, 但是你要看内部实现的方法, IFace<string>
的 Print
方法参数是 string
, 但是 IFace<object>
的 Print
参数是 object
, 如果上面的赋值可以成立, 就意味着允许 Print(string input)
方法传递任意类型的对象, 这样明显是有问题的
Q:   但是我曾经看到过 IEnumerable<object> list = new List<string>();
这个为什么就可以
A:   这就要讲到C#泛型里的逆变协变了
Q:   细嗦细嗦
逆变协变
C#泛型中的逆变(in)协变(out)对于不常自定义泛型的开发来说(可能)是个很难理解的概念, 简单来说其表现形式如下
逆变(in): I<子类> = I<父类>
协变(out): I<父类> = I<子类>
上面例子中提到的 IEnumerable<object> list = new List<string>();
体现的是协变
, 符合一般直觉, 整体上看起来就像是将子类赋值给基类
转到 IEnumerable<>
的定义, 我们可以看到
public interface IEnumerable<out T> : IEnumerable
{
new IEnumerator<T> GetEnumerator();
}
泛型 T
之前加了协变的关键词 out
, 代表支持协变, 可以进行符合直觉且和谐的转化
前编中提到的代码例子不适用并且也不能改造成协变, 只适合使用逆变
相比于符合直觉且和谐的协变, 逆变是不符合直觉并且别扭的
IFace<string> item = new Face<object>();
public interface IFace<in T>
{
string Print(T input);
}
public class Face<T> : IFace<T>
{
public string Print(T input) => input.ToString();
}
这是一个逆变的例子, 与协变相似, 需要在泛型 T
之前加上关键词 in
对比上方的协变, 逆变看起来就像是将基类赋值给子类, 但这其实符合里氏代换的
当我们调用 item.Print
时, 看起来允许传入的参数为 string
类型, 而实际上最终调用的 Face<object>.Print
是支持 object
的, 传入 string
类型的参数没有任何问题
逆变协变的作用
逆变(in)协变(out)的作用就是扩展泛型的用法, 帮助开发者更好地复用代码, 同时通过约束限制可能会出现的破坏类型安全的操作
逆变协变的限制
虽然上面讲了逆变(in)协变(out)看起来是什么样的, 但我的那个朋友还是有些疑问
Q:   那我什么时候可以用逆变, 什么时候可以用协变, 这两个东西用起来有什么限制?
A:   简单来说, 有关泛型输入
的用逆变
, 关键词是in
, 有关泛型输出
的用协变
, 关键词是out
, 如果接口中既有输入又有输出, 就不能用逆变协变
Q:   为什么这两个不能同时存在?
A:   协变
的表现形式为将子类赋值给基类
, 当进行输出
相关操作时, 输出的对象类型为基类, 是将子类转为基类, 你可以说子类是基类;逆变
的表现形式为将基类赋值给子类
, 当进行输入
相关操作时, 输入的对象为子类, 是将子类转为基类, 这个时候你也可以说基类是子类;
如果同时支持逆变协变, 若先进行子类赋值给基类的操作, 此时输出
的是基类, 子类转为基类并不会有什么问题, 但进行输入
操作时就是在将基类转为子类, 此时是无法保证类型安全的;
Q:   听不懂, 能不能举个例子给我?
A:   假设 IEnumerable<>
同时支持逆变协变, IEnumerable<object> list = new List<string>();
进行赋值后, list
中实际保存的类型是string
, item.First()
的输出
类型为object
, 实际类型是string
, 此时说string
是object
没有任何问题, 协变可以正常发挥作用;
但是如果支持了逆变, 假设我们进行输入
类型的操作, item.Add()
允许的参数类型为 object
, 可以是任意类型, 但是实际上支持string
类型, 此时的object
绝无可能是string
Q:   好像听懂了一点了, 我以后慢慢琢磨吧
两者的限制简单总结就是
输入
的用逆变
输出
的用协变
来源:https://www.cnblogs.com/CollapseNav/p/17285595.html


猜你喜欢
- 本篇文章基于redisson-3.17.6版本源码进行分析一、主从redis架构中分布式锁存在的问题1、线程A从主redis中请求一个分布式
- 重写子pagerview的dispatchTouchEvent方法,在返回前添加一句getParent().requestDisallowI
- 前提之前很长一段时间关注JDK协程库的开发进度,但是前一段时间比较忙很少去查看OpenJDK官网的内容。Java协程项目Loom(因为项目还
- 本文实例讲述了ActiveMQ在C#中的应用。分享给大家供大家参考,具体如下:ActiveMQ是个好东东,不必多说。ActiveMQ提供多种
- using System;using System.Collections.Generic;using System.ComponentMo
- 前言今天重新装了IDEA2020,顺带重装了一些插件,毕竟这些插件都是习惯一直在用,其中一款就是Mybatis Log plugin,按照往
- Spring底层核心原理下面这几行代码是一个Spring的入门代码,第一行是通过java配置类 注解的方式创建一个Spring容器,第二行是
- 记录单击、双击实现过程,进行简单的封装,便于复用,包括常用的软件双击退出。双击实现:记录第一次点击时间,在设定时间内再次点击,则返回监听事件
- 什么是ContentType?我们知道浏览器可以处理各种各样的内容,比如:HTML、XML、JPG、Flash等等,那么浏览器是如何区分它们
- 本文实例为大家分享了Android实现比赛时间闪动效果的具体代码,供大家参考,具体内容如下效果代码上代码public class Twink
- 最近项目做完了,有闲暇时间,一直想做一个类似微信中微信发说说,既能实现拍照,选图库,多图案上传的案例,目前好多App都有类似微信朋友圈的功能
- javax.persistence中@Column定义字段类型在@Column中有个比较强大的配置 columnDefinition,如果有
- 开发前准备1、密钥工具在线工具地址:https://miniu.alipay.com/keytool/create无需下载,直接在线生成你的
- 前言在java生态圈谈到Rpc,很多人可能就会想到Dubbo、Motan、Grpc等框架。但是你知道吗?作为Java编程全家桶的Spring
- 一、环境准备:(根据自己电脑配置来选择安装版本,我的电脑是64位,所以此处选择64位安装)JDK下载:JDK 1.8下载地址: http:/
- 本文实例为大家分享了OpenCV实现人脸识别程序的具体代码,供大家参考,具体内容如下//Haar特征检测,人脸识别算法,是用xml作为训练后
- 环境: idea2020.1插件: LeetCode-editor 6.7一、IDEA安装LeetCode插件安装完成重启idea打开插件U
- 最近碰到个需要下载zip压缩包的需求,于是我在网上找了下别人写好的zip工具类。但找了好多篇博客,总是发现有bug。因此就自己来写了个工具类
- 前言枚举为我看日常开发的可读性提供的非常好的支持,但是有时在使用枚举类型时,我们需要取名称和值,甚至有时候还需要取枚举类型的描述。通过反射,
- 一直想在持续集成方向学习并研究一番,近期正准备结合jmeter+ant+jenkins做自动化接口测试,在学习的同时,正好实践一番,毕竟实践