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: &ensp; string 明明是 object 的子类, 为啥这样赋值会报错呢???
A: &ensp; 因为 Face<string> 实现的是 IFace<string>, 而 IFace<string> 并不是 IFace<object> 的子类
Q: &ensp; 但是 string 是 object 的子类啊, IFace<string> 可不就是 IFace<object> 吗?
A: &ensp; 如果只论接口定义, 看起来确实是这样的, 但是你要看内部实现的方法, IFace<string> 的 Print 方法参数是 string, 但是 IFace<object> 的 Print 参数是 object, 如果上面的赋值可以成立, 就意味着允许 Print(string input) 方法传递任意类型的对象, 这样明显是有问题的
Q: &ensp; 但是我曾经看到过 IEnumerable<object> list = new List<string>(); 这个为什么就可以
A: &ensp; 这就要讲到C#泛型里的逆变协变
Q: &ensp; 细嗦细嗦

逆变协变

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: &ensp; 那我什么时候可以用逆变, 什么时候可以用协变, 这两个东西用起来有什么限制?

A: &ensp; 简单来说, 有关泛型输入的用逆变, 关键词是in, 有关泛型输出的用协变, 关键词是out, 如果接口中既有输入又有输出, 就不能用逆变协变

Q: &ensp; 为什么这两个不能同时存在?

A: &ensp; 协变的表现形式为将子类赋值给基类, 当进行输出相关操作时, 输出的对象类型为基类, 是将子类转为基类, 你可以说子类是基类;
逆变的表现形式为将基类赋值给子类, 当进行输入相关操作时, 输入的对象为子类, 是将子类转为基类, 这个时候你也可以说基类是子类;
如果同时支持逆变协变, 若先进行子类赋值给基类的操作, 此时输出的是基类, 子类转为基类并不会有什么问题, 但进行输入操作时就是在将基类转为子类, 此时是无法保证类型安全的;

Q: &ensp; 听不懂, 能不能举个例子给我?

A: &ensp; 假设 IEnumerable<> 同时支持逆变协变, IEnumerable<object> list = new List<string>();进行赋值后, list中实际保存的类型是stringitem.First()输出类型为object, 实际类型是string, 此时说stringobject没有任何问题, 协变可以正常发挥作用;
但是如果支持了逆变, 假设我们进行输入类型的操作, item.Add() 允许的参数类型为 object, 可以是任意类型, 但是实际上支持string类型, 此时的object绝无可能是string

Q: &ensp; 好像听懂了一点了, 我以后慢慢琢磨吧

两者的限制简单总结就是

输入的用逆变

输出的用协变

来源:https://www.cnblogs.com/CollapseNav/p/17285595.html

标签:C#泛型,c#逆变协变
0
投稿

猜你喜欢

  • Android Studio实现简单计算器功能

    2023-10-17 03:54:42
  • Java中id,pid格式数据转树和森林结构工具类实现

    2021-07-10 08:46:17
  • springboot-mybatis/JPA流式查询的多种实现方式

    2021-07-07 17:25:51
  • Android编程实现点击EditText之外的控件隐藏软键盘功能

    2022-08-07 02:06:08
  • java中String、StringBuffer与StringBuilder的区别

    2021-11-12 13:28:24
  • Java 数据结构进阶二叉树题集下

    2022-07-11 19:16:18
  • Spring Boot从Controller层进行单元测试的实现

    2023-07-21 03:07:10
  • DrawerLayout的简单使用及侧滑菜单实现详解

    2023-04-27 04:10:59
  • Java基于Swing实现的打猎射击游戏代码

    2021-06-29 17:04:56
  • Java基础入门总结之序列化和反序列化

    2023-02-19 03:29:10
  • C#委托delegate实例解析

    2021-07-28 08:58:26
  • C#日期格式字符串的相互转换操作实例分析

    2021-09-01 10:06:56
  • 全网最全SpringBoot集成swagger的详细教程

    2021-10-25 16:53:44
  • Spring Boot集成Shiro实现动态加载权限的完整步骤

    2023-02-18 17:43:10
  • c# List find()方法返回值的问题说明(返回结果为对象的指针)

    2023-11-20 21:55:34
  • Java如何通过线程解决生产者/消费者问题

    2023-09-27 00:31:08
  • Spring框架学习之Cache抽象详解

    2023-07-20 17:37:47
  • C#比较时间大小的方法总结

    2023-09-02 04:38:06
  • IDEA无法使用Git Pull的问题

    2023-05-04 10:55:39
  • c# 将Minio.exe注册成windows服务

    2022-09-25 20:51:18
  • asp之家 软件编程 m.aspxhome.com