当前位置: 首页 > news >正文

C#中泛型的协变和逆变

协变:

在泛型接口中,使用out关键字可以声明协变。这意味着接口的泛型参数只能作为返回类型出现,而不能作为方法的参数类型。

示例:泛型接口中的协变

假设我们有一个基类Animal和一个派生类Dog

csharp复制

public class Animal { }
public class Dog : Animal { }

接下来,定义一个协变的泛型接口IEnumerable<out T>,其中out关键字表示泛型参数T是协变的:

csharp复制

public interface IEnumerable<out T>
{IEnumerator<T> GetEnumerator();
}

在实际使用中,可以将派生类型的集合赋值给基类型的集合:

csharp复制

IEnumerable<Dog> dogs = new List<Dog> { new Dog(), new Dog() };
IEnumerable<Animal> animals = dogs; // 协变使得这行代码合法

这里,IEnumerable<Dog>可以赋值给IEnumerable<Animal>,因为DogAnimal的派生类。

限制

协变是C#中一种强大的类型转换机制,它使得代码更加灵活,同时保持类型安全。

  • 协变只能应用于返回类型,不能应用于方法的参数类型。例如,以下代码是非法的:

  • csharp复制

    public interface IExample<out T>
    {void Set(T value); // 错误:协变类型不能作为方法的参数
    }

  • 2. 泛型委托中的协变

    在泛型委托中,同样可以使用out关键字来实现协变。协变允许将派生类型的委托赋值给基类型的委托。

    示例:泛型委托中的协变

    假设我们有以下基类和派生类:

  • csharp复制

    public class Animal { }
    public class Dog : Animal { }

    定义一个协变的泛型委托Func<out T>

    csharp复制

    public delegate T Func<out T>();

    在实际使用中,可以将派生类型的委托赋值给基类型的委托:

    csharp复制

    Func<Dog> getDog = () => new Dog();
    Func<Animal> getAnimal = getDog; // 协变使得这行代码合法

    这里,Func<Dog>可以赋值给Func<Animal>,因为DogAnimal的派生类。

    限制
  • 协变委托只能应用于返回类型,不能应用于委托的参数类型。例如,以下代码是非法的

  • csharp复制

    public delegate void Action<out T>(T value); // 错误:协变类型不能作为委托的参数

  • 3. 实际代码示例

    以下是一个完整的代码示例,展示如何在泛型接口和委托中使用协变:

  • using System;
    using System.Collections.Generic;// 定义基类和派生类
    public class Animal { }
    public class Dog : Animal { }// 泛型接口中的协变
    public interface IEnumerable<out T>
    {IEnumerator<T> GetEnumerator();
    }// 泛型委托中的协变
    public delegate T Func<out T>();class Program
    {static void Main(){// 泛型接口中的协变IEnumerable<Dog> dogs = new List<Dog> { new Dog(), new Dog() };IEnumerable<Animal> animals = dogs; // 协变使得这行代码合法Console.WriteLine("泛型接口中的协变成功!");// 泛型委托中的协变Func<Dog> getDog = () => new Dog();Func<Animal> getAnimal = getDog; // 协变使得这行代码合法Console.WriteLine("泛型委托中的协变成功!");}
    }

    4. 总结

  • 泛型接口中的协变:通过在接口中使用out关键字,可以将派生类型的实例赋值给基类型的变量。

  • 泛型委托中的协变:通过在委托中使用out关键字,可以将派生类型的委托赋值给基类型的委托。

  • 限制:协变只能应用于返回类型,不能应用于方法的参数类型或委托的参数类型。

C#中,本来派生类就可以直接给基类赋值,那为什么还要使用协变呢?优点在哪里?

确实,C#中派生类的实例可以直接赋值给基类的变量,这是面向对象编程中的多态性(Polymorphism)的基本特性。然而,协变(Covariance)的作用并不仅仅局限于将派生类对象赋值给基类变量,它更多地是为了在泛型编程中提供更灵活的类型转换能力,同时保持类型安全。

1. 协变的背景和动机

在C#中,泛型类型(如List<T>)和泛型接口(如IEnumerable<T>)在设计时,默认是不变的(Invariant)。这意味着即使DogAnimal的派生类,List<Dog>也不能直接赋值给List<Animal>IEnumerable<Dog>也不能直接赋值给IEnumerable<Animal>。例如:

csharp复制

List<Dog> dogs = new List<Dog>();
List<Animal> animals = dogs; // 错误:不能直接赋值
IEnumerable<Dog> dogEnumerable = dogs;
IEnumerable<Animal> animalEnumerable = dogEnumerable; // 同样错误

这种限制在某些场景下显得过于严格,尤其是在处理泛型集合或委托时。协变的引入正是为了解决这种类型转换的限制。

2. 协变的优点

(1)更灵活的类型转换

协变允许将派生类型的泛型集合或委托赋值给基类型的泛型集合或委托。这使得代码更加灵活,减少了不必要的类型转换和冗余代码。例如:

csharp复制

IEnumerable<Dog> dogs = new List<Dog> { new Dog(), new Dog() };
IEnumerable<Animal> animals = dogs; // 协变使得这行代码合法

如果没有协变,你需要手动将IEnumerable<Dog>转换为IEnumerable<Animal>,这不仅繁琐,还可能引入错误。

(2)保持类型安全

协变的使用是安全的,因为它只允许将派生类型的集合或委托赋值给基类型的集合或委托。你不能将基类型的集合赋值给派生类型的集合(这会破坏类型安全)。例如:

csharp复制

IEnumerable<Animal> animals = new List<Animal>();
IEnumerable<Dog> dogs = animals; // 错误:逆变或显式转换

这种限制确保了类型安全,避免了运行时错误。

(3)简化代码设计

协变使得泛型接口和委托的使用更加自然,减少了对泛型参数的限制。例如,在委托中使用协变可以简化方法的返回类型处理:

csharp复制

Func<Dog> getDog = () => new Dog();
Func<Animal> getAnimal = getDog; // 协变使得这行代码合法

如果没有协变,你需要为每种类型定义不同的委托,这会增加代码的复杂性。

(4)与.NET框架的兼容性

.NET框架中许多核心接口(如IEnumerable<T>IEnumerator<T>)都支持协变。这使得开发者可以更自然地使用这些接口,而无需担心类型转换问题。例如,IEnumerable<T>的协变特性使得LINQ操作更加灵活。


3. 协变与多态的区别

虽然派生类可以直接赋值给基类变量,但这种多态性仅适用于非泛型类型。对于泛型类型(如List<T>IEnumerable<T>),默认情况下是不变的。协变的引入正是为了解决这种泛型类型转换的限制。

例如:

  • 多态性Dog可以直接赋值给Animal,因为Dog继承自Animal

  • 协变IEnumerable<Dog>可以赋值给IEnumerable<Animal>,因为Dog继承自Animal,并且IEnumerable<T>支持协变。

如果没有协变,IEnumerable<Dog>IEnumerable<Animal>之间无法直接转换,这会限制泛型的灵活性。

4. 总结

协变的主要优点在于它为泛型编程提供了更灵活的类型转换能力,同时保持了类型安全。它使得泛型接口和委托的使用更加自然,减少了不必要的类型转换和冗余代码。虽然派生类可以直接赋值给基类变量,但这种多态性并不适用于泛型类型。协变的引入正是为了解决这种限制,使得泛型编程更加强大和灵活。

逆变:

逆变(Contravariance)在C#中主要用于泛型接口和委托,允许将基类类型的参数传递给期望派生类类型的方法或委托。这种特性在某些特定场景下非常有用,尤其是在需要提高代码复用性和灵活性时。以下是逆变在具体场景中的应用示例:


1. 泛型接口中的逆变

逆变可以用于泛型接口,允许将一个实现基类接口的对象赋值给派生类接口的变量。这在比较器接口(如IComparer<in T>)和动作接口(如IAction<in T>)中非常常见。

示例:比较器接口

假设有一个基类Animal和派生类Dog

csharp复制

public class Animal { }
public class Dog : Animal { }

定义一个支持逆变的泛型接口IComparer<in T>

csharp复制

public interface IComparer<in T>
{int Compare(T x, T y);
}

实现一个比较器,用于比较Animal对象:

csharp复制

public class AnimalComparer : IComparer<Animal>
{public int Compare(Animal x, Animal y){// 比较逻辑return x.ToString().CompareTo(y.ToString());}
}

由于IComparer<in T>支持逆变,可以将AnimalComparer赋值给IComparer<Dog>

csharp复制

IComparer<Dog> dogComparer = new AnimalComparer();

优点:通过逆变,可以复用AnimalComparer来比较Dog对象,而无需为每个派生类单独实现比较器。

2. 委托中的逆变

逆变也支持委托,允许将一个接受基类类型参数的方法赋值给期望派生类类型参数的委托。这在事件处理、回调函数等场景中非常有用。

示例:事件处理

假设有一个基类Animal和派生类Dog

csharp复制

public class Animal { }
public class Dog : Animal { }

定义一个支持逆变的委托Action<in T>

csharp复制

public delegate void Action<in T>(T item);

实现一个方法,用于处理Animal对象:

csharp复制

void HandleAnimal(Animal animal)
{Console.WriteLine("Handling an Animal");
}

由于Action<in T>支持逆变,可以将HandleAnimal方法赋值给Action<Dog>

csharp复制

Action<Dog> handleDog = HandleAnimal;
handleDog(new Dog()); // 输出:Handling an Animal

优点通过逆变,可以使用一个通用的HandleAnimal方法来处理Dog对象,而无需为每个派生类单独实现处理方法。

自己总结:

协变:即平常使用的派生类就可以赋值给基类,但是当你用了List或者其他泛型的时候,就没那么好赋值,需要各种类型显示转换,这个时候协变就显得特别好用。

逆变:当我们拥有一个通讯的基类,各种通讯均继承这个基类,批量处理派生类的时候,可以将基类运用逆变的方法,作为派生类的参数,使用统一模板。

还有更好的理解,欢迎评论~~~

相关文章:

C#中泛型的协变和逆变

协变&#xff1a; 在泛型接口中&#xff0c;使用out关键字可以声明协变。这意味着接口的泛型参数只能作为返回类型出现&#xff0c;而不能作为方法的参数类型。 示例&#xff1a;泛型接口中的协变 假设我们有一个基类Animal和一个派生类Dog&#xff1a; csharp复制 public…...

【JavaScript】《JavaScript高级程序设计 (第4版) 》笔记-附录B-严格模式

附录B、严格模式 严格模式 ECMAScript 5 首次引入严格模式的概念。严格模式用于选择以更严格的条件检查 JavaScript 代码错误&#xff0c;可以应用到全局&#xff0c;也可以应用到函数内部。严格模式的好处是可以提早发现错误&#xff0c;因此可以捕获某些 ECMAScript 问题导致…...

跨平台 C++ 程序崩溃调试与 Dump 文件分析

前言 C 程序在运行时可能会由于 空指针访问、数组越界、非法内存访问、栈溢出 等原因崩溃。为了分析崩溃原因&#xff0c;我们通常会生成 Dump 文件&#xff08;Windows 的 .dmp&#xff0c;Linux 的 core&#xff0c;macOS 的 .crash&#xff09;&#xff0c;然后用调试工具分…...

缺陷VS质量:为何软件缺陷是质量属性的致命对立面?

为何说缺陷是质量的对立面&#xff1f; 核心逻辑&#xff1a;软件质量的定义是“满足用户需求的程度”&#xff0c;而缺陷会直接破坏这种满足关系。 对立性&#xff1a;缺陷的存在意味着软件偏离了预期行为&#xff08;如功能错误、性能不足、安全性漏洞等&#xff09;&#…...

伍[5],伺服电机,电流环,速度环,位置环

电流环、速度环和位置环是电机控制系统中常见的三个闭环控制环节,通常采用嵌套结构(内环→外环:电流环→速度环→位置环),各自负责不同层级的控制目标。以下是它们的详细说明及相互关系: 1. 电流环(最内环) 作用:控制电机的电流,间接控制输出转矩(τ=Kt⋅Iτ=Kt​⋅…...

RuntimeError: CUDA error: device-side assert triggered

RuntimeError: CUDA error: device-side assert triggered 欢迎来到英杰社区&#xff0c;这里是博主英杰https://bbs.csdn.net/topics/617804998 原因&#xff1a; cuda运行可能是异步的&#xff08;asynchronously&#xff09;&#xff0c;因此报错信息中提示的位置可能不准确…...

清华大学Deepseek第六版AIGC发展研究3.0(共186页,附PDF下载)

人工智能生成内容&#xff08;AIGC&#xff09;正以前所未有的速度改变我们的生活。 2024年底&#xff0c;清华大学新闻与传播学院与人工智能学院联合发布了《AIGC发展研究3.0版》&#xff0c;这份报告系统梳理了AIGC技术的突破性进展、应用场景及社会影响&#xff0c;并展望了…...

SpringBoot生成唯一ID的方式

1.为什么要生成唯一ID&#xff1f; 数据唯一性&#xff1a;每个记录都需要有一个独一无二的标识符来确保数据的唯一性。这可以避免重复的数据行&#xff0c;并有助于准确地查询、更新或删除特定的记录。 数据完整性&#xff1a;通过使用唯一ID&#xff0c;可以保证数据库中的数…...

通俗易懂的分类算法之K近邻详解

通俗易懂的分类算法之K近邻详解 用最通俗的语言和例子&#xff0c;来彻底理解 K近邻&#xff08;K-Nearest Neighbors&#xff0c;简称 KNN&#xff09; 这个分类算法。不用担心复杂的数学公式&#xff0c;我会用生活中的例子来解释&#xff0c;保证你一听就懂&#xff01; 1.…...

CSDN markdown 操作指令等

CSDN markdown 操作指令等 页内跳转 [内容](#1) <div id"1"> </div>...

【linux】文件与目录命令 - uniq

文章目录 1. 基本用法2. 常用参数3. 用法举例4. 注意事项 uniq 命令用于过滤文本文件中相邻的重复行&#xff0c;并支持统计重复次数或仅保留唯一行。它通常与 sort 命令配合使用&#xff0c;因为 uniq 只识别相邻的重复行。 1. 基本用法 语法&#xff1a; uniq [选项] [输入…...

零信任沙箱:为网络安全筑牢“隔离墙”

在数字化浪潮汹涌澎湃的今天&#xff0c;网络安全如同一艘船在波涛汹涌的大海中航行&#xff0c;面临着重重挑战。数据泄露、恶意软件攻击、网络钓鱼等安全威胁层出不穷&#xff0c;让企业和个人用户防不胜防。而零信任沙箱&#xff0c;就像是一座坚固的“隔离墙”&#xff0c;…...

【金融量化】Ptrade中交易环境支持的业务类型

1. 普通股票买卖 • 特点&#xff1a; 普通股票买卖是最基础的交易形式&#xff0c;投资者通过买入和卖出上市公司的股票来获取收益。 ◦ 流动性高&#xff1a;股票市场交易活跃&#xff0c;买卖方便。 ◦ 收益来源多样&#xff1a;包括股价上涨的资本利得和公司分红。 ◦ 风险…...

【Java---数据结构】链表 LinkedList

1. 链表的概念 链表用于存储一系列元素&#xff0c;由一系列节点组成&#xff0c;每个节点包含两部分&#xff1a;数据域和指针域。 数据域&#xff1a;用于存储数据元素 指针域&#xff1a;用于指向下一个节点的地址&#xff0c;通过指针将各个节点连接在一起&#xff0c;形…...

紧跟 Web3 热潮,RuleOS 如何成为行业新宠?

Web3 热潮正以汹涌之势席卷全球。从金融领域的创新应用到供应链管理的变革&#xff0c;从社交媒体的去中心化尝试到游戏产业的全新玩法探索&#xff0c;Web3 凭借其去中心化、安全性和用户赋权等特性&#xff0c;为各个行业带来了前所未有的机遇。在这股热潮中&#xff0c;Rule…...

CC++的内存管理

目录 1、C/C内存划分 C语言的动态内存管理 malloc calloc realloc free C的动态内存管理 new和delete operator new函数和operator delete函数 new和delete的原理 new T[N]原理 delete[]的原理 1、C/C内存划分 1、栈&#xff1a;存有非静态局部变量、函数参数、返回…...

Spark核心之02:RDD、算子分类、常用算子

spark内存计算框架 一、目标 深入理解RDD弹性分布式数据集底层原理掌握RDD弹性分布式数据集的常用算子操作 二、要点 ⭐️1. RDD是什么 RDD&#xff08;Resilient Distributed Dataset&#xff09;叫做**弹性分布式数据集&#xff0c;是Spark中最基本的数据抽象&#xff0c…...

【Resis实战分析】Redis问题导致页面timeout知识点分析

事故现象&#xff1a;前端页面返回timeout 事故回溯总结一句话&#xff1a; &#xff08;1&#xff09;因为大KEY调用量&#xff0c;随着白天自然流量趋势增长而增长&#xff0c;最终在业务高峰最高点期占满带宽使用100%。 &#xfeff; &#xfeff; &#xff08;2&#x…...

单一职责原则(设计模式)

目录 问题&#xff1a; 定义&#xff1a; 解决&#xff1a; 方式 1&#xff1a;使用策略模式 示例&#xff1a;用户管理 方式 2&#xff1a;使用装饰者模式 示例&#xff1a;用户操作 方式 3&#xff1a;使用责任链模式 示例&#xff1a;用户操作链 总结 推荐 问题&a…...

生理信号概念

rPPG 信号&#xff08;远程光电容积脉搏波信号&#xff09; 原理&#xff1a; 基于光电容积脉搏波描记法&#xff0c;利用普通摄像头&#xff0c;在一定距离外捕捉人体皮肤表面因心脏泵血导致的血液容积变化引起的细微颜色变化&#xff0c;通过图像处理和信号分析算法提取心率…...

React 第五十五节 Router 中 useAsyncError的使用详解

前言 useAsyncError 是 React Router v6.4 引入的一个钩子&#xff0c;用于处理异步操作&#xff08;如数据加载&#xff09;中的错误。下面我将详细解释其用途并提供代码示例。 一、useAsyncError 用途 处理异步错误&#xff1a;捕获在 loader 或 action 中发生的异步错误替…...

Linux链表操作全解析

Linux C语言链表深度解析与实战技巧 一、链表基础概念与内核链表优势1.1 为什么使用链表&#xff1f;1.2 Linux 内核链表与用户态链表的区别 二、内核链表结构与宏解析常用宏/函数 三、内核链表的优点四、用户态链表示例五、双向循环链表在内核中的实现优势5.1 插入效率5.2 安全…...

RocketMQ延迟消息机制

两种延迟消息 RocketMQ中提供了两种延迟消息机制 指定固定的延迟级别 通过在Message中设定一个MessageDelayLevel参数&#xff0c;对应18个预设的延迟级别指定时间点的延迟级别 通过在Message中设定一个DeliverTimeMS指定一个Long类型表示的具体时间点。到了时间点后&#xf…...

51c自动驾驶~合集58

我自己的原文哦~ https://blog.51cto.com/whaosoft/13967107 #CCA-Attention 全局池化局部保留&#xff0c;CCA-Attention为LLM长文本建模带来突破性进展 琶洲实验室、华南理工大学联合推出关键上下文感知注意力机制&#xff08;CCA-Attention&#xff09;&#xff0c;…...

转转集团旗下首家二手多品类循环仓店“超级转转”开业

6月9日&#xff0c;国内领先的循环经济企业转转集团旗下首家二手多品类循环仓店“超级转转”正式开业。 转转集团创始人兼CEO黄炜、转转循环时尚发起人朱珠、转转集团COO兼红布林CEO胡伟琨、王府井集团副总裁祝捷等出席了开业剪彩仪式。 据「TMT星球」了解&#xff0c;“超级…...

MVC 数据库

MVC 数据库 引言 在软件开发领域,Model-View-Controller(MVC)是一种流行的软件架构模式,它将应用程序分为三个核心组件:模型(Model)、视图(View)和控制器(Controller)。这种模式有助于提高代码的可维护性和可扩展性。本文将深入探讨MVC架构与数据库之间的关系,以…...

MODBUS TCP转CANopen 技术赋能高效协同作业

在现代工业自动化领域&#xff0c;MODBUS TCP和CANopen两种通讯协议因其稳定性和高效性被广泛应用于各种设备和系统中。而随着科技的不断进步&#xff0c;这两种通讯协议也正在被逐步融合&#xff0c;形成了一种新型的通讯方式——开疆智能MODBUS TCP转CANopen网关KJ-TCPC-CANP…...

代理篇12|深入理解 Vite中的Proxy接口代理配置

在前端开发中,常常会遇到 跨域请求接口 的情况。为了解决这个问题,Vite 和 Webpack 都提供了 proxy 代理功能,用于将本地开发请求转发到后端服务器。 什么是代理(proxy)? 代理是在开发过程中,前端项目通过开发服务器,将指定的请求“转发”到真实的后端服务器,从而绕…...

服务器--宝塔命令

一、宝塔面板安装命令 ⚠️ 必须使用 root 用户 或 sudo 权限执行&#xff01; sudo su - 1. CentOS 系统&#xff1a; yum install -y wget && wget -O install.sh http://download.bt.cn/install/install_6.0.sh && sh install.sh2. Ubuntu / Debian 系统…...

Java + Spring Boot + Mybatis 实现批量插入

在 Java 中使用 Spring Boot 和 MyBatis 实现批量插入可以通过以下步骤完成。这里提供两种常用方法&#xff1a;使用 MyBatis 的 <foreach> 标签和批处理模式&#xff08;ExecutorType.BATCH&#xff09;。 方法一&#xff1a;使用 XML 的 <foreach> 标签&#xff…...