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 信号(远程光电容积脉搏波信号) 原理: 基于光电容积脉搏波描记法,利用普通摄像头,在一定距离外捕捉人体皮肤表面因心脏泵血导致的血液容积变化引起的细微颜色变化,通过图像处理和信号分析算法提取心率…...
Leetcode 3576. Transform Array to All Equal Elements
Leetcode 3576. Transform Array to All Equal Elements 1. 解题思路2. 代码实现 题目链接:3576. Transform Array to All Equal Elements 1. 解题思路 这一题思路上就是分别考察一下是否能将其转化为全1或者全-1数组即可。 至于每一种情况是否可以达到…...
Opencv中的addweighted函数
一.addweighted函数作用 addweighted()是OpenCV库中用于图像处理的函数,主要功能是将两个输入图像(尺寸和类型相同)按照指定的权重进行加权叠加(图像融合),并添加一个标量值&#x…...
从零实现STL哈希容器:unordered_map/unordered_set封装详解
本篇文章是对C学习的STL哈希容器自主实现部分的学习分享 希望也能为你带来些帮助~ 那咱们废话不多说,直接开始吧! 一、源码结构分析 1. SGISTL30实现剖析 // hash_set核心结构 template <class Value, class HashFcn, ...> class hash_set {ty…...
pikachu靶场通关笔记22-1 SQL注入05-1-insert注入(报错法)
目录 一、SQL注入 二、insert注入 三、报错型注入 四、updatexml函数 五、源码审计 六、insert渗透实战 1、渗透准备 2、获取数据库名database 3、获取表名table 4、获取列名column 5、获取字段 本系列为通过《pikachu靶场通关笔记》的SQL注入关卡(共10关࿰…...
七、数据库的完整性
七、数据库的完整性 主要内容 7.1 数据库的完整性概述 7.2 实体完整性 7.3 参照完整性 7.4 用户定义的完整性 7.5 触发器 7.6 SQL Server中数据库完整性的实现 7.7 小结 7.1 数据库的完整性概述 数据库完整性的含义 正确性 指数据的合法性 有效性 指数据是否属于所定…...
【电力电子】基于STM32F103C8T6单片机双极性SPWM逆变(硬件篇)
本项目是基于 STM32F103C8T6 微控制器的 SPWM(正弦脉宽调制)电源模块,能够生成可调频率和幅值的正弦波交流电源输出。该项目适用于逆变器、UPS电源、变频器等应用场景。 供电电源 输入电压采集 上图为本设计的电源电路,图中 D1 为二极管, 其目的是防止正负极电源反接, …...
JavaScript基础-API 和 Web API
在学习JavaScript的过程中,理解API(应用程序接口)和Web API的概念及其应用是非常重要的。这些工具极大地扩展了JavaScript的功能,使得开发者能够创建出功能丰富、交互性强的Web应用程序。本文将深入探讨JavaScript中的API与Web AP…...
android RelativeLayout布局
<?xml version"1.0" encoding"utf-8"?> <RelativeLayout xmlns:android"http://schemas.android.com/apk/res/android"android:layout_width"match_parent"android:layout_height"match_parent"android:gravity&…...
tomcat指定使用的jdk版本
说明 有时候需要对tomcat配置指定的jdk版本号,此时,我们可以通过以下方式进行配置 设置方式 找到tomcat的bin目录中的setclasspath.bat。如果是linux系统则是setclasspath.sh set JAVA_HOMEC:\Program Files\Java\jdk8 set JRE_HOMEC:\Program Files…...
深度学习之模型压缩三驾马车:模型剪枝、模型量化、知识蒸馏
一、引言 在深度学习中,我们训练出的神经网络往往非常庞大(比如像 ResNet、YOLOv8、Vision Transformer),虽然精度很高,但“太重”了,运行起来很慢,占用内存大,不适合部署到手机、摄…...
