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):用out关键字指定类型参数是协变的,用于输出参数,如方法的返回值类型。表现为子类隐式转换为父类,就是标准的里式替换原则。
- 逆变(Contravariance):用in关键字指定类型参数是逆变的,一般用于输入,如方法的参数。表现为协变相反的转换过程,但其实本质上(在方法参数上)还是里式替换原则。
📢
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》
©️版权申明:版权所有@安木夕,本文内容仅供学习,欢迎指正、交流,转载请注明出处!原文编辑地址-语雀__
2025-07-07 13:57:34【出处】:https://www.cnblogs.com/anding/p/18940828
=======================================================================================
如果,您希望更容易地发现我的新博客,不妨点击一下绿色通道的【关注我】。(●'◡'●)
因为,我的写作热情也离不开您的肯定与支持,感谢您的阅读,我是【Jack_孟】!
本文来自博客园,作者:jack_Meng,转载请注明原文链接:https://www.cnblogs.com/mq0036/p/18970447
【免责声明】本文来自源于网络,如涉及版权或侵权问题,请及时联系我们,我们将第一时间删除或更改!
浙公网安备 33010602011771号