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)
协变,逆变与不变 能在使用父类型的场景中改用子类型的被称为协变. 能在使用子类型的场景中改用父类型的被称为逆变. 不能做到以上两点的被称为不变. 以上的场景通常包括数组,继承和泛型. 协变逆变与泛型( ...
随机推荐
- 《HelloGitHub》第 108 期
兴趣是最好的老师,HelloGitHub 让你对开源感兴趣! 简介 HelloGitHub 分享 GitHub 上有趣.入门级的开源项目. github.com/521xueweihan/HelloG ...
- 【Git】基本操作
一.Git 基础 1.Git 介绍 Git 是目前世界上最先进的分布式版本控制系统. 版本控制系统: 设计师在设计的时候做了很多版本 经过了数天去问设计师每个版本都改了些啥,设计师此时可能就说不上来了 ...
- 古老的BAT文件在windows下的编辑
直接上代码 rem echo choice /t 20 /d y /n >nul rem start/d "E:\service\exe" 8091.exe 8091 sta ...
- Hyperledger Fabric - 自定义network.sh脚本
引言:依据hyperledger fabric提供的测试网络脚本搭建自己的网络环境 该系列参考:https://blog.csdn.net/ling1998?type=blog 执行./network ...
- 自动驾驶 | 为CarLA添加一辆小米SU7 Part I
自动驾驶 | 为CarLA添加一辆小米SU7 Part I 导言 什么是CarLA? CarLA是一款基于虚幻引擎4(Unreal Engine 4)构建的开源自动驾驶仿真平台,为自动驾驶算法的研发. ...
- Redis底层数据结构-quicklist、listpack
quicklist 在 Redis 3.0 之前,List 对象的底层数据结构是双向链表或者压缩列表.然后在 Redis 3.2 的时候,List 对象的底层改由 quicklist 数据结构实现. ...
- 【完结】【一本通提高】KMP做题记录
题目编号 标题 估分 正确 提交 Y 2076 Problem A [一本通提高篇KMP]剪花布条 --- 156 293 Y 2077 Problem B [一本通提高篇KMP]Radio Tr ...
- 通用型产品发布解决方案(SpringBoot+SpringCloud+Spring CloudAlibaba+Vue+ElementUI+MyBatis-Plus+MySQL+Git+Maven)03
通用型产品发布解决方案(基于分布式微服务技术栈:SpringBoot+SpringCloud+Spring CloudAlibaba+Vue+ElementUI+MyBatis-Plus+MySQL+ ...
- 89.4K star!这个开源LLM应用开发平台,让你轻松构建AI工作流!
嗨,大家好,我是小华同学,关注我们获得"最新.最全.最优质"开源项目和高效工作学习方法 Dify 是一款开源的 LLM 应用开发平台,通过直观的可视化界面整合 AI 工作流.RAG ...
- Web前端入门第 51 问:移动端适配的视口元标签(meta)常见使用场景
经常查看网页源码的同学应该都有注意到,基本上面向移动端的所有网站都有个 <meta name="viewport" xxx> 这样的HTML元素. 为什么需要 meta ...