C#泛型的逆变协变(个人理解)
前编
一般来说, 泛型的作用就类似一个占位符, 或者说是一个参数, 可以让我们把类型像参数一样进行传递, 尽可能地复用代码
我有个朋友, 在使用的过程中发现一个问题
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: 好像听懂了一点了, 我以后慢慢琢磨吧
两者的限制简单总结就是
输入的用逆变
输出的用协变
C#泛型的逆变协变(个人理解)的更多相关文章
- Java泛型的逆变
在上篇<Java泛型的协变>这篇文章中遗留以下问题——协变不能解决将子类型添加到父类型的泛型列表中.本篇将用逆变来解决这个问题. 实验准备 我们首先增加以下方法,见代码清单1所示. 代码清 ...
- .NET Core CSharp初级篇 1-8泛型、逆变与协变
.NET Core CSharp初级篇 1-8 本节内容为泛型 为什么需要泛型 泛型是一个非常有趣的东西,他的出现对于减少代码复用率有了很大的帮助.比如说遇到两个模块的功能非常相似,只是一个是处理in ...
- ios开发ios9新特性关键字学习:泛型,逆变,协变,__kindof
一:如何去学习?都去学习什么? 1:学习优秀项目的设计思想,多问几个为什么,为什么要这么设计,这么设计的好处是什么,还能不能在优化 ,如何应用到自己的项目中 2:学习优秀项目的代码风格,代码的封装设计 ...
- .NET中的逆变协变
MSDN上的说法: 协变和逆变都是术语,前者指能够使用比原始指定的派生类型的派生程度更小(不太具体的)的类型,后者指能够使用比原始指定的派生类型的派生程度更大(更具体的)的类型----------(注 ...
- 【温故而知新-万花筒】C# 异步编程 逆变 协变 委托 事件 事件参数 迭代 线程、多线程、线程池、后台线程
额基本脱离了2.0 3.5的时代了.在.net 4.0+ 时代.一切都是辣么简单! 参考文档: http://www.cnblogs.com/linzheng/archive/2012/04/11/2 ...
- C#-逆变 协变 反射 代码
首先看一段测试代码,自己写的 class Program { static void Main(string[] args) { man OneMan = new man(); var d = One ...
- C# 泛型的协变和逆变
1. 可变性的类型:协变性和逆变性 可变性是以一种类型安全的方式,将一个对象当做另一个对象来使用.如果不能将一个类型替换为另一个类型,那么这个类型就称之为:不变量.协变和逆变是两个相互对立的概念: 如 ...
- c# 协变和逆变的理解
1. 是什么 1.1 协变 协变指能够使用比原始指定的派生类型的派生程度更小(不太具体的)的类型.如 string 到 object 的转换.多见于类型参数用作方法的返回值. 1.2 逆变 逆变指能够 ...
- C# 泛型的协变和逆变 (转载)
1. 可变性的类型:协变性和逆变性 可变性是以一种类型安全的方式,将一个对象当做另一个对象来使用.如果不能将一个类型替换为另一个类型,那么这个类型就称之为:不变量. 协变和逆变是两个相互对立的概念: ...
- 在net中json序列化与反序列化 面向对象六大原则 (第一篇) 一步一步带你了解linq to Object 10分钟浅谈泛型协变与逆变
在net中json序列化与反序列化 准备好饮料,我们一起来玩玩JSON,什么是Json:一种数据表示形式,JSON:JavaScript Object Notation对象表示法 Json语法规则 ...
随机推荐
- iOS学习十二之选择器控件UIPickerView
UIPickerView是一个简易的列表控件,用于提供有限个数的选项供用户选择. 它是通过代理和数据源的方法对其进行设置和数据源填充的,这种控件的设计模式也是代理模式的应用之一. 添加下面的代码即可实 ...
- 创建一个HashMap实例,该实例具有足够高的“初始容量”
创建一个HashMap实例,该实例具有足够高的"初始容量" /** * 创建一个{@link HashMap}实例,该实例具有足够高的"初始容量" * * @p ...
- 类和动态内存分配的课后习题(C++ prime plus)
第一题 1. 对于下面的类声明: class Cow { char name[20]; char *hobby; double weight; public: Cow(); Cow(const cha ...
- scanf 读入 string 注意点
在做题的时候需要读入字符串,但是又不想使用char 数组,于是采用string存储,当时遇到了scanf读取string失败,查阅资料后总结下. scanf是c的标准输入输出流,想要读入string, ...
- 初步学习UE网络同步
UE 网络同步和框架介绍 为一个UE引擎的初学者基于现有知识储备和见识的限制下,对UE网络和游戏框架的粗鄙之见,文中多有错误敬请指出以较后文. 1.网络复制 不论是服务端还是客户端,代码都是一样的 ...
- idea好用的功能
1. 自定义快捷输入 ----https://blog.csdn.net/qq_35091353/article/details/11828025 可以将一些常用的语法,比如各种lamda表达式.tr ...
- Docker系列--Docker设置系统资源限制及验证
1.限制容器的资源 默认情况下,容器没有资源限制,可以使用主机内核调度程序允许的尽可能多的给定资源.Docker提供了控制容器可以使用多少内存或CPU的方法,设置docker run命令的运行时配置标 ...
- Spring系列之类路径扫描和注册组件-8
目录 类路径扫描和注册组件 `@Component` 使用元注释和组合注释 自动检测类和注册 Bean 定义 使用过滤器自定义扫描 在组件中定义 Bean 元数据 命名自动检测到的组件 为自动检测的组 ...
- PLC入门笔记6
计数器指令及其应用 计数器指令介绍 很多场合需要进行计数操作.例如电机启动次数.生产线物料经过次数.位置传感器传送的脉冲次数等. 计数器分为普通和高速两种. 比PLC扫描频率远小于用普通,接近或大于用 ...
- mybatis的sql操作
1.Mapper接口创建 创建Mapper包,所有的Mapper接口放在该包下.Mapper接口中声明将要实现的方法,在接下来的Mapper.xml文件中实现对应方法. 2.Mapper.xml创建 ...