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>
,因为Dog
是Animal
的派生类。
限制
协变是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>
,因为Dog
是Animal
的派生类。限制
-
协变委托只能应用于返回类型,不能应用于委托的参数类型。例如,以下代码是非法的
-
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)。这意味着即使Dog
是Animal
的派生类,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#中泛型的协变和逆变
协变: 在泛型接口中,使用out关键字可以声明协变。这意味着接口的泛型参数只能作为返回类型出现,而不能作为方法的参数类型。 示例:泛型接口中的协变 假设我们有一个基类Animal和一个派生类Dog: csharp复制 public…...
【JavaScript】《JavaScript高级程序设计 (第4版) 》笔记-附录B-严格模式
附录B、严格模式 严格模式 ECMAScript 5 首次引入严格模式的概念。严格模式用于选择以更严格的条件检查 JavaScript 代码错误,可以应用到全局,也可以应用到函数内部。严格模式的好处是可以提早发现错误,因此可以捕获某些 ECMAScript 问题导致…...
跨平台 C++ 程序崩溃调试与 Dump 文件分析
前言 C 程序在运行时可能会由于 空指针访问、数组越界、非法内存访问、栈溢出 等原因崩溃。为了分析崩溃原因,我们通常会生成 Dump 文件(Windows 的 .dmp,Linux 的 core,macOS 的 .crash),然后用调试工具分…...
缺陷VS质量:为何软件缺陷是质量属性的致命对立面?
为何说缺陷是质量的对立面? 核心逻辑:软件质量的定义是“满足用户需求的程度”,而缺陷会直接破坏这种满足关系。 对立性:缺陷的存在意味着软件偏离了预期行为(如功能错误、性能不足、安全性漏洞等)&#…...
伍[5],伺服电机,电流环,速度环,位置环
电流环、速度环和位置环是电机控制系统中常见的三个闭环控制环节,通常采用嵌套结构(内环→外环:电流环→速度环→位置环),各自负责不同层级的控制目标。以下是它们的详细说明及相互关系: 1. 电流环(最内环) 作用:控制电机的电流,间接控制输出转矩(τ=Kt⋅Iτ=Kt⋅…...

RuntimeError: CUDA error: device-side assert triggered
RuntimeError: CUDA error: device-side assert triggered 欢迎来到英杰社区,这里是博主英杰https://bbs.csdn.net/topics/617804998 原因: cuda运行可能是异步的(asynchronously),因此报错信息中提示的位置可能不准确…...
清华大学Deepseek第六版AIGC发展研究3.0(共186页,附PDF下载)
人工智能生成内容(AIGC)正以前所未有的速度改变我们的生活。 2024年底,清华大学新闻与传播学院与人工智能学院联合发布了《AIGC发展研究3.0版》,这份报告系统梳理了AIGC技术的突破性进展、应用场景及社会影响,并展望了…...
SpringBoot生成唯一ID的方式
1.为什么要生成唯一ID? 数据唯一性:每个记录都需要有一个独一无二的标识符来确保数据的唯一性。这可以避免重复的数据行,并有助于准确地查询、更新或删除特定的记录。 数据完整性:通过使用唯一ID,可以保证数据库中的数…...
通俗易懂的分类算法之K近邻详解
通俗易懂的分类算法之K近邻详解 用最通俗的语言和例子,来彻底理解 K近邻(K-Nearest Neighbors,简称 KNN) 这个分类算法。不用担心复杂的数学公式,我会用生活中的例子来解释,保证你一听就懂! 1.…...
CSDN markdown 操作指令等
CSDN markdown 操作指令等 页内跳转 [内容](#1) <div id"1"> </div>...
【linux】文件与目录命令 - uniq
文章目录 1. 基本用法2. 常用参数3. 用法举例4. 注意事项 uniq 命令用于过滤文本文件中相邻的重复行,并支持统计重复次数或仅保留唯一行。它通常与 sort 命令配合使用,因为 uniq 只识别相邻的重复行。 1. 基本用法 语法: uniq [选项] [输入…...

零信任沙箱:为网络安全筑牢“隔离墙”
在数字化浪潮汹涌澎湃的今天,网络安全如同一艘船在波涛汹涌的大海中航行,面临着重重挑战。数据泄露、恶意软件攻击、网络钓鱼等安全威胁层出不穷,让企业和个人用户防不胜防。而零信任沙箱,就像是一座坚固的“隔离墙”,…...
【金融量化】Ptrade中交易环境支持的业务类型
1. 普通股票买卖 • 特点: 普通股票买卖是最基础的交易形式,投资者通过买入和卖出上市公司的股票来获取收益。 ◦ 流动性高:股票市场交易活跃,买卖方便。 ◦ 收益来源多样:包括股价上涨的资本利得和公司分红。 ◦ 风险…...

【Java---数据结构】链表 LinkedList
1. 链表的概念 链表用于存储一系列元素,由一系列节点组成,每个节点包含两部分:数据域和指针域。 数据域:用于存储数据元素 指针域:用于指向下一个节点的地址,通过指针将各个节点连接在一起,形…...
紧跟 Web3 热潮,RuleOS 如何成为行业新宠?
Web3 热潮正以汹涌之势席卷全球。从金融领域的创新应用到供应链管理的变革,从社交媒体的去中心化尝试到游戏产业的全新玩法探索,Web3 凭借其去中心化、安全性和用户赋权等特性,为各个行业带来了前所未有的机遇。在这股热潮中,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、栈:存有非静态局部变量、函数参数、返回…...

Spark核心之02:RDD、算子分类、常用算子
spark内存计算框架 一、目标 深入理解RDD弹性分布式数据集底层原理掌握RDD弹性分布式数据集的常用算子操作 二、要点 ⭐️1. RDD是什么 RDD(Resilient Distributed Dataset)叫做**弹性分布式数据集,是Spark中最基本的数据抽象,…...

【Resis实战分析】Redis问题导致页面timeout知识点分析
事故现象:前端页面返回timeout 事故回溯总结一句话: (1)因为大KEY调用量,随着白天自然流量趋势增长而增长,最终在业务高峰最高点期占满带宽使用100%。   (2&#x…...
单一职责原则(设计模式)
目录 问题: 定义: 解决: 方式 1:使用策略模式 示例:用户管理 方式 2:使用装饰者模式 示例:用户操作 方式 3:使用责任链模式 示例:用户操作链 总结 推荐 问题&a…...
生理信号概念
rPPG 信号(远程光电容积脉搏波信号) 原理: 基于光电容积脉搏波描记法,利用普通摄像头,在一定距离外捕捉人体皮肤表面因心脏泵血导致的血液容积变化引起的细微颜色变化,通过图像处理和信号分析算法提取心率…...

UE5 学习系列(二)用户操作界面及介绍
这篇博客是 UE5 学习系列博客的第二篇,在第一篇的基础上展开这篇内容。博客参考的 B 站视频资料和第一篇的链接如下: 【Note】:如果你已经完成安装等操作,可以只执行第一篇博客中 2. 新建一个空白游戏项目 章节操作,重…...
pam_env.so模块配置解析
在PAM(Pluggable Authentication Modules)配置中, /etc/pam.d/su 文件相关配置含义如下: 配置解析 auth required pam_env.so1. 字段分解 字段值说明模块类型auth认证类模块,负责验证用户身份&am…...
Linux简单的操作
ls ls 查看当前目录 ll 查看详细内容 ls -a 查看所有的内容 ls --help 查看方法文档 pwd pwd 查看当前路径 cd cd 转路径 cd .. 转上一级路径 cd 名 转换路径 …...
Golang dig框架与GraphQL的完美结合
将 Go 的 Dig 依赖注入框架与 GraphQL 结合使用,可以显著提升应用程序的可维护性、可测试性以及灵活性。 Dig 是一个强大的依赖注入容器,能够帮助开发者更好地管理复杂的依赖关系,而 GraphQL 则是一种用于 API 的查询语言,能够提…...

C++ 求圆面积的程序(Program to find area of a circle)
给定半径r,求圆的面积。圆的面积应精确到小数点后5位。 例子: 输入:r 5 输出:78.53982 解释:由于面积 PI * r * r 3.14159265358979323846 * 5 * 5 78.53982,因为我们只保留小数点后 5 位数字。 输…...

涂鸦T5AI手搓语音、emoji、otto机器人从入门到实战
“🤖手搓TuyaAI语音指令 😍秒变表情包大师,让萌系Otto机器人🔥玩出智能新花样!开整!” 🤖 Otto机器人 → 直接点明主体 手搓TuyaAI语音 → 强调 自主编程/自定义 语音控制(TuyaAI…...

CMake 从 GitHub 下载第三方库并使用
有时我们希望直接使用 GitHub 上的开源库,而不想手动下载、编译和安装。 可以利用 CMake 提供的 FetchContent 模块来实现自动下载、构建和链接第三方库。 FetchContent 命令官方文档✅ 示例代码 我们将以 fmt 这个流行的格式化库为例,演示如何: 使用 FetchContent 从 GitH…...

tree 树组件大数据卡顿问题优化
问题背景 项目中有用到树组件用来做文件目录,但是由于这个树组件的节点越来越多,导致页面在滚动这个树组件的时候浏览器就很容易卡死。这种问题基本上都是因为dom节点太多,导致的浏览器卡顿,这里很明显就需要用到虚拟列表的技术&…...

Qt的学习(一)
1.什么是Qt Qt特指用来进行桌面应用开发(电脑上写的程序)涉及到的一套技术Qt无法开发网页前端,也不能开发移动应用。 客户端开发的重要任务:编写和用户交互的界面。一般来说和用户交互的界面,有两种典型风格&…...

【iOS】 Block再学习
iOS Block再学习 文章目录 iOS Block再学习前言Block的三种类型__ NSGlobalBlock____ NSMallocBlock____ NSStackBlock__小结 Block底层分析Block的结构捕获自由变量捕获全局(静态)变量捕获静态变量__block修饰符forwarding指针 Block的copy时机block作为函数返回值将block赋给…...