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

相关文章

Vue3 的 shallowRef 和 shallowReactive:优化性能

大家对 Vue3 的 ref 和 reactive 都很熟悉,那么对 shallowRef 和 shallowReactive 是否了解呢? 在编程和数据结构中,“shallow”(浅层)通常指对数据结构的最外层进行操作,而不递归地处理其内部或嵌套的数据。这种处理方式关注的是数据结构的第一层属性或元素,而忽略更深层次的嵌套内容。 1. 浅层与深层的对比 1.1 浅层(Shallow) 定义

Ilya-AI分享的他在OpenAI学习到的15个提示工程技巧

Ilya(不是本人,claude AI)在社交媒体上分享了他在OpenAI学习到的15个Prompt撰写技巧。 以下是详细的内容: 提示精确化:在编写提示时,力求表达清晰准确。清楚地阐述任务需求和概念定义至关重要。例:不用"分析文本",而用"判断这段话的情感倾向:积极、消极还是中性"。 快速迭代:善于快速连续调整提示。熟练的提示工程师能够灵活地进行多轮优化。例:从"总结文章"到"用

闲置电脑也能活出第二春?鲁大师AiNAS让你动动手指就能轻松部署

对于大多数人而言,在这个“数据爆炸”的时代或多或少都遇到过存储告急的情况,这使得“存储焦虑”不再是个别现象,而将会是随着软件的不断臃肿而越来越普遍的情况。从不少手机厂商都开始将存储上限提升至1TB可以见得,我们似乎正处在互联网信息飞速增长的阶段,对于存储的需求也将会不断扩大。对于苹果用户而言,这一问题愈发严峻,毕竟512GB和1TB版本的iPhone可不是人人都消费得起的,因此成熟的外置存储方案开

性能测试介绍

性能测试是一种测试方法,旨在评估系统、应用程序或组件在现实场景中的性能表现和可靠性。它通常用于衡量系统在不同负载条件下的响应时间、吞吐量、资源利用率、稳定性和可扩展性等关键指标。 为什么要进行性能测试 通过性能测试,可以确定系统是否能够满足预期的性能要求,找出性能瓶颈和潜在的问题,并进行优化和调整。 发现性能瓶颈:性能测试可以帮助发现系统的性能瓶颈,即系统在高负载或高并发情况下可能出现的问题

性能分析之MySQL索引实战案例

文章目录 一、前言二、准备三、MySQL索引优化四、MySQL 索引知识回顾五、总结 一、前言 在上一讲性能工具之 JProfiler 简单登录案例分析实战中已经发现SQL没有建立索引问题,本文将一起从代码层去分析为什么没有建立索引? 开源ERP项目地址:https://gitee.com/jishenghua/JSH_ERP 二、准备 打开IDEA找到登录请求资源路径位置

2. c#从不同cs的文件调用函数

1.文件目录如下: 2. Program.cs文件的主函数如下 using System;using System.Collections.Generic;using System.Linq;using System.Threading.Tasks;using System.Windows.Forms;namespace datasAnalysis{internal static

【数据结构】——原来排序算法搞懂这些就行,轻松拿捏

前言:快速排序的实现最重要的是找基准值,下面让我们来了解如何实现找基准值 基准值的注释:在快排的过程中,每一次我们要取一个元素作为枢纽值,以这个数字来将序列划分为两部分。 在此我们采用三数取中法,也就是取左端、中间、右端三个数,然后进行排序,将中间数作为枢纽值。 快速排序实现主框架: //快速排序 void QuickSort(int* arr, int left, int rig

活用c4d官方开发文档查询代码

当你问AI助手比如豆包,如何用python禁止掉xpresso标签时候,它会提示到 这时候要用到两个东西。https://developers.maxon.net/论坛搜索和开发文档 比如这里我就在官方找到正确的id描述 然后我就把参数标签换过来

Linux 网络编程 --- 应用层

一、自定义协议和序列化反序列化 代码: 序列化反序列化实现网络版本计算器 二、HTTP协议 1、谈两个简单的预备知识 https://www.baidu.com/ --- 域名 --- 域名解析 --- IP地址 http的端口号为80端口,https的端口号为443 url为统一资源定位符。CSDNhttps://mp.csdn.net/mp_blog/creation/editor

【Python编程】Linux创建虚拟环境并配置与notebook相连接

1.创建 使用 venv 创建虚拟环境。例如,在当前目录下创建一个名为 myenv 的虚拟环境: python3 -m venv myenv 2.激活 激活虚拟环境使其成为当前终端会话的活动环境。运行: source myenv/bin/activate 3.与notebook连接 在虚拟环境中,使用 pip 安装 Jupyter 和 ipykernel: pip instal