C#.Net筑基-泛型T & 协变逆变
01、什么是泛型?
泛型(Generics)是C#中的一种强大的强类型扩展机制,在申明时用“占位符”类型参数“T”定义一个“模板类型”,比较类似于C++中的模板。泛型在使用时指定具体的T类型,从而方便的封装、复用代码,提高类型的安全性,减少类型转换和装箱。
- 泛型就是为代码能 跨类型复用 而设计的,轻松复用代码逻辑,如
List<T>
、Queue<T>
。 - 用泛型参数来代替object,可以减少大量装箱、拆箱,显著提高代码性能,及代码安全性。比如C#中的
List<T>
就是泛型版的ArrayList
,Dictionary<TKey, TValue>
就是泛型版的Hashtable
,非泛型版本就不建议使用了。
1.1、泛型知识点集合
知识点 | 说明 |
---|---|
泛型类型 | 类、结构体、接口、委托:申明时类型名后指定一个或多个泛型参数,class User<T,TV,TP>{} 。 |
泛型方法 | 在方法上指定泛型参数,public T Add<T>(T x,T y){} 。 |
泛型参数“T” | 用尖括号<T> 的语法引入泛型参数“T",表示这是一个泛型类、或泛型方法,支持一个或多个泛型参数。 |
泛型参数“T”命名 | 一般用“T”或T开头来占位,表示一个模板类型,名称可自定义 |
泛型约束where | 对泛型参数T的条件约束,限定T的类型、范围,更方便的封装代码 class User<T> where T : struct |
开放类型 List<T> |
未指定泛型参数的类型叫“开放类型”,不能直接使用。只有身体,没有灵魂,并不完整。 |
封闭类型 List<int> |
指定了泛型参数后的泛型为“封闭类型”,才是完整的类型,才可以实例化,这里的泛型参数为int 。 |
静态成员共享 | 泛型类型中的静态成员,所有封闭类型是共享的。 |
- 构造函数不可引入泛型参数。
- 不同数量的泛型参数可以“重载”,
interface IUser<T>
,interface IUser<T1,T2>
是不同的两个泛型类型。
02、泛型约束where
如果没有约束,泛型参数“T”可以用任何类型来替代。泛型约束可以约束泛型参数“T”的范围,然后可以利用约束类型的一些能力,这也是泛型比较强大的地方之一。
- 约束条件用
where:[约束1],[约束2]
语法申明,跟在泛型申明后面,可以跟多个约束条件,逗号隔开。 - 多个泛型参数,可用多个
where
分别约束。
泛型约束条件 | 约束说明 |
---|---|
class | 必须为引用类型,可以是任何类、接口、委托、数组。 |
struct | 必须为非null值类型。 |
notnull | 不为 null 的类型。 |
unmanaged | “非托管类型”类型,内置的基础值类型如byte、int、char、float、double、bool、枚举、指针等。 |
new() | 类型必须有无参构造函数,当与其他约束一起使用时,new() 约束必须在最后。 |
Delegate | 类型必须为委托 |
Enum | 枚举类型 |
INumber<T> |
类型为内置数值类型,如int 、double ,这个不错,非常便于封装数值相关的代码!如数学运算。 |
具体接口、类型 | 任意明确的类型、接口作为约束条件,不能是封闭类型(封闭类型用泛型就没有意义了) |
其他泛型类型 | 约束类型可以继续用其他泛型类型,where T2 : IUser<T2> |
相互约束 | 泛型参数之间相互约束,class MyClass<T, U> where T : U 约束类型T 和U 兼容 |
自引用约束 | 用自身类型作为约束,interface IUser<T> where T : IUser<T> |
并不是所有类型都可以用于约束,严格来说只有接口、未封闭的类才能用与类型约束,不支持的有
int
、float
、double
、Array
、数组、ValueType
等,Object是万物基类,也不能作为约束,这个约束等于没有约束。
- 泛型约束最大的好处就是可以利用约束的能力,实现更方便的封装。
public T Create<T>() where T : new() //约束了T可以new,就是具备无参构造函数
{
return new T(); //这里就可用这个约束能力了
}
public T Max<T>(T a, T b) where T : IComparable<T> //约束实现了比较接口
{
return a.CompareTo(b) > 0 ? a : b;
}
// Max(1,2); //2
public class GClass<T, TV>
where T : IComparable<T>, new()
where TV : struct, INumber<TV> //数值类型,可以用数学运算了
{
public TV Value { get; set; }
public void Add(TV value)
{
this.Value += value;
}
}
.NET 7 中实现的 INumber-TSelf 添加了大量数学运算接口,C#内置的数值类型(int、float、double等)都实现了该接口。官方文档《泛型数学》还有更多更细的数学运算接口。
03、协变与逆变
C#中的协变与逆变,本质就是灵活控制类型的向上(父类)转换,即保障类型安全,又兼顾灵活性和代码的复用性。这里就不得不回顾下向对象的基本原则之一——里氏替换原则。
3.1、里氏替换原则
里氏替换原则 (Liskov Substitution Principle,LSP)是面向对象编程中的基本原则之一,在各种编程语言中使用广泛。其定义为:派生类(子类)对象可以代替其基类(超类)对象,这也是面向对象多态的体现。
就是说任何子类都可以替代其父类,或者说子类可以安全的转换为父类。在接口或类的继承中,向上转换是安全的,这也是继承的基本特点。如下面的方法Foo(object value)
,可以传入任意object
的子类,因为所有类型都继承自object
。
void Main()
{
string s = null;
object o = s; //父类兼容子类,string 隐式转换为 object
Foo("sam"); //类型匹配 string隐式转换为object
Foo(new User()); //类型匹配 User隐式转换为object
Foo<string>("sam"); //类型一致
Foo<object>("sam"); //类型匹配 string隐式转换为object
}
public object Foo(object obj)
{
return "sam"; //string隐式转换为object
}
public void Foo<T>(T obj) { }
日常编程中也常常用到里氏替换原则,用子类代替父类使用。
void Main()
{
SetUser(new User());
SetUser(new Teacher()); //输入参数用子类代替
}
public class User { }
public class Teacher : User { }
public User FindUser(){
return new Teacher(); //返回值用子类代替
}
public void SetUser(User user){}
在C#中,里氏替换原则的表现就是子类可以隐式转换为父类,如上面的示例,在方法调用、返回值、赋值时都支持向上的隐式转换。但是在泛型中,这却行不通,如下示例代码,这就不符合上面的里氏替换原则了,影响了编程的灵活性。
interface IFoo<T>{}
IFoo<string> s2 = default;
IFoo<object> o2 = s2; //不可隐式转换,报错
//添加out参数后,可隐式转换
interface IFoo<out T>{}
在泛型中,是需要严格类型匹配的,才能保障类型的安全。在某些场景但为了兼顾灵活性、复用性,便有了协变、逆变。
3.2、协变(Covariance/out)、逆变(Contravariance/in)
为了在泛型中支持上述隐式转换,就有协变、逆变。当然这不仅仅用于泛型,委托中的协变、逆变和泛型是一样的,还有C#中的数组是支持协变的。
- 协变(Covariance):用
<font style="color:#D22D8D;">out</font>
关键字指定类型参数是协变的,用于输出参数,如方法的返回值类型。表现为子类隐式转换为父类,就是标准的里式替换原则。 - 逆变(Contravariance):用
<font style="color:#D22D8D;">in</font>
关键字指定类型参数是逆变的,一般用于输入,如方法的参数。表现为协变相反的转换过程,但其实本质上(在方法参数上)还是里式替换原则。
out
、in
关键字只能用在泛型接口、泛型委托上。
下面是C#中内置的协变、逆变使用场景。
//数组是内置支持协变的,只支持引用类型,不支持值类型
object[] arr = new string[10];
object[] us = new User[2];
//C#中的IEnumerator<T>源码
public interface IEnumerator<out T> : IDisposable, IEnumerator
{
new T Current { get; }
}
//C#中的Func<T1,T2,TResult>源码
public delegate TResult Func<in T1, in T2, out TResult>(T1 arg1, T2 arg2);
协变out
的示例代码:
IFoo<string>
隐式转换为IFoo<object>
,子类(泛型)转父类(泛型),协变。- 在方法返回值上,
IFoo<object>.Func()
返回object
,支持返回string
(IFoo<object>
),string
转object
,是符合里氏替换原则的。
void Main()
{
IFoo<string> f1 = new Foo();
IFoo<object> f2 = f1; //协变,IFoo<string> 隐式转换为IFoo<object>
}
interface IFoo<out T> //如果没有out,则上面的转换抛出异常
{
public T Func();
}
class Foo : IFoo<string>
{
public string Func()
{
return "sam";
}
}
逆变in
的示例代码:
IFoo<object>
隐式转换为IFoo<string>
,父类(泛型)转子类(泛型),相反的方向,逆变。- 在方法参数上,
IFoo<object>
参数为object
,支持用string
(IFoo<string>
),string
转object
,是符合里氏替换原则的。
void Main()
{
IFoo<object> f1 = new Foo();
IFoo<string> f2 = f1; //逆变,IFoo<object> 隐式转换为IFoo<string>
}
interface IFoo<in T> //如果没有in,则上面的转换抛出异常
{
public void Func(T value);
}
class Foo : IFoo<object>
{
public void Func(object value) { }
}
warning
协变、逆变只是其表象,其本质是一样的,就是里氏替换原则!(可用子类替换为父类)
参考资料
- .NET 中的泛型
- 类型参数的约束(C# 编程指南)
- 深入理解C#的协变和逆变及其限制原因
- 《C#8.0 In a Nutshell》
️版权申明:版权所有@安木夕,本文内容仅供学习,欢迎指正、交流,转载请注明出处!原文编辑地址-语雀__
C#.Net筑基-泛型T & 协变逆变的更多相关文章
- 解读经典《C#高级编程》最全泛型协变逆变解读 页127-131.章4
前言 本篇继续讲解泛型.上一篇讲解了泛型类的定义细节.本篇继续讲解泛型接口. 泛型接口 使用泛型可定义接口,即在接口中定义的方法可以带泛型参数.然后由继承接口的类实现泛型方法.用法和继承泛型类基本没有 ...
- C#中泛型方法与泛型接口 C#泛型接口 List<IAll> arssr = new List<IAll>(); interface IPerson<T> c# List<接口>小技巧 泛型接口协变逆变的几个问题
http://blog.csdn.net/aladdinty/article/details/3486532 using System; using System.Collections.Generi ...
- C#的in/out关键字与协变逆变
C#提供了一组关键字in&out,在泛型接口和泛型委托中,若不使用关键字修饰类型参数T,则该类型参数是不可变的(即不允许协变/逆变转换),若使用in修饰类型参数T,保证"只将T用于输 ...
- 编写高质量代码改善C#程序的157个建议——建议45:为泛型类型参数指定逆变
建议45:为泛型类型参数指定逆变 逆变是指方法的参数可以是委托或者泛型接口的参数类型的基类.FCL4.0中支持逆变的常用委托有: Func<int T,out TResult> Predi ...
- Programming In Scala笔记-第十九章、类型参数,协变逆变,上界下界
本章主要讲Scala中的类型参数化.本章主要分成三个部分,第一部分实现一个函数式队列的数据结构,第二部分实现该结构的内部细节,最后一个部分解释其中的关键知识点.接下来的实例中将该函数式队列命名为Que ...
- java协变逆变,PECS
public static void main(String[] args) { // Object <- Fruit <- Apple <- RedApple System.out ...
- 协变 & 逆变
都跟里氏替换原则有关. 协变:你可以用一个子类对象去替换相应的一个父类对象,这是完全符合里氏替换原则的,和协(谐)的变.如:用Swan替换Bird. 逆变:你可以用一个父类对象去替换相应的一个子类对象 ...
- C#核心语法讲解-泛型(详细讲解泛型方法、泛型类、泛型接口、泛型约束,了解协变逆变)
泛型(generic)是C#语言2.0和通用语言运行时(CLR)的一个新特性.泛型为.NET框架引入了类型参数(type parameters)的概念.类型参数使得设计类和方法时,不必确定一个或多个具 ...
- C#核心语法-泛型(详细讲解泛型方法、泛型类、泛型接口、泛型约束,了解协变逆变)
泛型(generic)是C#语言2.0和通用语言运行时(CLR)的一个新特性.泛型为.NET框架引入了类型参数(type parameters)的概念.类型参数使得设计类和方法时,不必确定一个或多个具 ...
- 协变(covariance),逆变(contravariance)与不变(invariance)
协变,逆变与不变 能在使用父类型的场景中改用子类型的被称为协变. 能在使用子类型的场景中改用父类型的被称为逆变. 不能做到以上两点的被称为不变. 以上的场景通常包括数组,继承和泛型. 协变逆变与泛型( ...
随机推荐
- 【Ubuntu】在Ubuntu上安装IDEA
[Ubuntu]在Ubuntu上安装IDEA 零.前言 最近换了Ubuntu系统,但是还得是要写代码,这样就不可避免地用到IDEA,接下来介绍一下如何在Ubuntu上安装IDEA. 壹.下载 这一步应 ...
- DotNetGuide 突破了 8K + Star,努力打造C#/.NET/.NET Core全面的学习、工作、面试指南知识库!
前言 转眼之间维护DotNetGuide(全面的C#/.NET/.NET Core学习.工作.面试指南知识库)已经持续超过了4年多的时间,Commit提交数也超过1400+,在前几天在 GitHub ...
- shell处理字符串
概念 字符串是shell编程中最常用最有用的数据类型(除了数字和字符串,也没啥其它类型好用了),字符串可以用单引号,也可以用双引号,也可以不用引号. 单引号声明字符串 单引号里的任何字符都会原样输出, ...
- Vue的前端项目开发环境搭建
一.本机window端:安装Node.js,其实质性功能相当于,java的maven https://nodejs.org/en/download/ 二.本机window端:检查Node.js的版本 ...
- Excel导入操作,poi
导入操作,仅供参考,具体情况具体而论 @Override public ReturnObject inforImport(LogySbjsJdsbqxxxParts entity, HttpServl ...
- 【工具】SageMath|Ubuntu 22 下 SageMath 安装和一般数域筛法代码示例(2024年)
就一个终端就能运行的东西, 网上写教程写那么长, 稍微短点的要么是没链接只有截图.要么是链接给的不到位, 就这,不是耽误生命吗. 废话就到这里. 文章目录 链接 步骤 链接 参考: Install S ...
- 【经验】Git|如何删除错误的commit?(存在大文件无法push的commit、不需要的commit等情况、清除所有commit的情况)
2024/04/24说明:这篇暂时修改为粉丝可见,因为正在冲粉丝量,等到我弄完了粉丝量的要求,我就改回来!不方便看到全文的小伙伴不好意思!! 文章目录 情况一:尚未推送或无法推送 情况二:已经推送 情 ...
- 私人问卷收集系统-Surveyking问卷收集系统
前言 但凡提及问卷收集系统,问卷星与腾讯问卷通常都为大家首选问卷调查系统. 担心数据安全,海量问卷管理不便,工作流创建困难?快速部署自有问卷调查系统开始你的问卷调查之旅. 无论是问卷调查,考试系统,公 ...
- TVM Pass优化 -- InferType 类型推导
定义(What) InferType,类型推断,顾名思义,给表达式进行类型的推断 直接上代码 import tvm from tvm import relay import numpy as np d ...
- VMware NSX Manager SSL证书更新
安装 NSX 后,管理器节点和集群具有自签名证书.证书有效期为825天,到期后需要进行证书重新更新.如图所示,本环境中此次将有三个类型的证书即将到期需要替换:1.NSX 联合身份验证 PI(Local ...