C#协变与逆变:解锁高级编程技巧,轻松提升代码性能

本文主要是介绍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参数,有了这个参数,就拥有了协变的功能,从而当UT的子类时,可以支持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>,但在IOutout修饰符的作用下,成功让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);}
}

逆变和协变最大的不同,并非inout这两个修饰符的字数,而是整个替换逻辑发生了变化,上述代码中,实际上是作为子类的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>中的方法,那么其流程如下

  1. A I<A>.test(A t),即输入一个A类型的参数
  2. 将这个A类型的参数t,传入到B I<B>.test(B t)。由于I<B>要求输入B类型的参数,所以要求A可以转换为B类型。
  3. B I<B>.test(B t)计算完毕,返回一个B类型的参数
  4. 这个B类型的参数又被返回给最初的调用者A I<A>.test,而这时I<A>的函数最终将返回一个A类型的参数,换言之,在这个步骤,要求B可以转换为A

A能转为B,然后还得B能转为A,同时AB还不相等,这显然是不可能的。

所以逆变和协变分别实现了第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>objectstring更宽松)
  • 方法的返回值要更加严格,此即协变(IOut<string>调用IOut<object>stringobject更严格)

这篇关于C#协变与逆变:解锁高级编程技巧,轻松提升代码性能的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/790597

相关文章

Python通过模块化开发优化代码的技巧分享

《Python通过模块化开发优化代码的技巧分享》模块化开发就是把代码拆成一个个“零件”,该封装封装,该拆分拆分,下面小编就来和大家简单聊聊python如何用模块化开发进行代码优化吧... 目录什么是模块化开发如何拆分代码改进版:拆分成模块让模块更强大:使用 __init__.py你一定会遇到的问题模www.

前端高级CSS用法示例详解

《前端高级CSS用法示例详解》在前端开发中,CSS(层叠样式表)不仅是用来控制网页的外观和布局,更是实现复杂交互和动态效果的关键技术之一,随着前端技术的不断发展,CSS的用法也日益丰富和高级,本文将深... 前端高级css用法在前端开发中,CSS(层叠样式表)不仅是用来控制网页的外观和布局,更是实现复杂交

揭秘Python Socket网络编程的7种硬核用法

《揭秘PythonSocket网络编程的7种硬核用法》Socket不仅能做聊天室,还能干一大堆硬核操作,这篇文章就带大家看看Python网络编程的7种超实用玩法,感兴趣的小伙伴可以跟随小编一起... 目录1.端口扫描器:探测开放端口2.简易 HTTP 服务器:10 秒搭个网页3.局域网游戏:多人联机对战4.

springboot循环依赖问题案例代码及解决办法

《springboot循环依赖问题案例代码及解决办法》在SpringBoot中,如果两个或多个Bean之间存在循环依赖(即BeanA依赖BeanB,而BeanB又依赖BeanA),会导致Spring的... 目录1. 什么是循环依赖?2. 循环依赖的场景案例3. 解决循环依赖的常见方法方法 1:使用 @La

使用C#代码在PDF文档中添加、删除和替换图片

《使用C#代码在PDF文档中添加、删除和替换图片》在当今数字化文档处理场景中,动态操作PDF文档中的图像已成为企业级应用开发的核心需求之一,本文将介绍如何在.NET平台使用C#代码在PDF文档中添加、... 目录引言用C#添加图片到PDF文档用C#删除PDF文档中的图片用C#替换PDF文档中的图片引言在当

详解C#如何提取PDF文档中的图片

《详解C#如何提取PDF文档中的图片》提取图片可以将这些图像资源进行单独保存,方便后续在不同的项目中使用,下面我们就来看看如何使用C#通过代码从PDF文档中提取图片吧... 当 PDF 文件中包含有价值的图片,如艺术画作、设计素材、报告图表等,提取图片可以将这些图像资源进行单独保存,方便后续在不同的项目中使

C#使用SQLite进行大数据量高效处理的代码示例

《C#使用SQLite进行大数据量高效处理的代码示例》在软件开发中,高效处理大数据量是一个常见且具有挑战性的任务,SQLite因其零配置、嵌入式、跨平台的特性,成为许多开发者的首选数据库,本文将深入探... 目录前言准备工作数据实体核心技术批量插入:从乌龟到猎豹的蜕变分页查询:加载百万数据异步处理:拒绝界面

用js控制视频播放进度基本示例代码

《用js控制视频播放进度基本示例代码》写前端的时候,很多的时候是需要支持要网页视频播放的功能,下面这篇文章主要给大家介绍了关于用js控制视频播放进度的相关资料,文中通过代码介绍的非常详细,需要的朋友可... 目录前言html部分:JavaScript部分:注意:总结前言在javascript中控制视频播放

C#数据结构之字符串(string)详解

《C#数据结构之字符串(string)详解》:本文主要介绍C#数据结构之字符串(string),具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录转义字符序列字符串的创建字符串的声明null字符串与空字符串重复单字符字符串的构造字符串的属性和常用方法属性常用方法总结摘

C#如何动态创建Label,及动态label事件

《C#如何动态创建Label,及动态label事件》:本文主要介绍C#如何动态创建Label,及动态label事件,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录C#如何动态创建Label,及动态label事件第一点:switch中的生成我们的label事件接着,