C# 泛型(Generic)(手把手讲解)
💡一则或许对你有用的小广告
欢迎加入小哈的星球 ,你将获得:专属的项目实战 / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论
- 新项目:《从零手撸:仿小红书(微服务架构)》 正在持续爆肝中,基于
Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...
,点击查看项目介绍 ;演示链接: http://116.62.199.48:7070 ;- 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/ ;
截止目前, 星球 内专栏累计输出 90w+ 字,讲解图 3441+ 张,还在持续爆肝中.. 后续还会上新更多项目,目标是将 Java 领域典型的项目都整一波,如秒杀系统, 在线商城, IM 即时通讯,权限管理,Spring Cloud Alibaba 微服务等等,已有 3100+ 小伙伴加入学习 ,欢迎点击围观
前言
在 C# 开发中,泛型(Generic)是一个既基础又强大的特性。它允许开发者通过参数化类型的方式,编写更灵活、高效且类型安全的代码。对于编程初学者而言,泛型可能是一个抽象的概念;而对中级开发者来说,深入理解泛型的底层逻辑和应用场景则能显著提升编码效率。本文将从泛型的起源、核心概念、实际案例到高级技巧,逐步展开讲解,帮助读者系统性掌握这一重要工具。
一、泛型是什么?为什么需要它?
1.1 泛型的定义
泛型是 C# 2.0 引入的一项语言特性,其核心思想是“类型参数化”。通过在类、接口、方法等类型声明时添加类型参数(如 <T>
),开发者可以在不指定具体类型的情况下,编写通用的代码逻辑。例如,一个泛型列表 List<T>
可以存储任意类型的元素(如 int
、string
或自定义类),而无需为每种类型单独实现一套代码。
1.2 泛型的诞生背景
在泛型出现之前,开发者通常通过 object
类型或继承 IList
接口来实现通用容器。例如:
// 非泛型列表的实现
public class MyList {
private object[] items;
public void Add(object item) {
// ...
}
public object Get(int index) {
return items[index];
}
}
但这种方式存在两个问题:
- 类型安全缺失:返回的
object
需要强制类型转换,容易引发InvalidCastException
。 - 性能损耗:频繁的装箱(Boxing)和拆箱(Unboxing)会降低程序效率。
泛型的出现解决了这些问题:
- 类型安全:通过类型参数
T
确保所有操作都基于同一类型。 - 零性能开销:编译时会为每个具体类型(如
int
、string
)生成专用代码,避免运行时的类型转换。
1.3 泛型的比喻
想象泛型像一个“万能快递箱”:
- 通用性:无论装的是书籍、衣服还是电子产品,箱子的结构都一致。
- 安全性:箱子的锁只能对应特定类型的物品(例如,书籍箱不能装大件家具)。
- 高效性:每个箱子根据内容物定制大小和材质,避免空间浪费。
二、泛型的基本使用
2.1 泛型类的定义与实例化
定义泛型类时,需在类名后添加类型参数列表,例如:
public class MyGenericClass<T> {
private T value;
public void Set(T input) {
value = input;
}
public T Get() {
return value;
}
}
使用时,指定具体类型即可:
var intBox = new MyGenericClass<int>();
intBox.Set(100);
int result = intBox.Get(); // 直接获取 int 类型,无需转换
2.2 泛型方法
泛型方法允许在方法层级定义类型参数,例如:
public class Util {
public static T GetDefaultValue<T>() {
return default(T);
}
}
调用时:
int zero = Util.GetDefaultValue<int>(); // 0
string empty = Util.GetDefaultValue<string>(); // ""
2.3 内置泛型集合
C# 提供了丰富的泛型集合类,如 List<T>
、Dictionary<TKey, TValue>
等。例如:
// 泛型列表的使用
List<string> names = new List<string>();
names.Add("Alice");
names.Add("Bob");
foreach (string name in names) {
Console.WriteLine(name); // 输出 Alice、Bob
}
三、泛型约束(Constraints)
3.1 约束的必要性
泛型的灵活性可能带来类型不兼容的风险。例如,若希望泛型类 MyGenericClass<T>
的 T
必须实现 IEnumerable
接口,则需添加约束。
3.2 约束类型与语法
C# 支持多种约束类型,常见用法如下表:
约束类型 | 语法 | 作用说明 |
---|---|---|
类型约束 | where T : SomeClass | T 必须是 SomeClass 或其子类 |
接口约束 | where T : IInterface | T 必须实现 IInterface 接口 |
new() 约束 | where T : new() | T 必须有无参构造函数 |
多重约束 | where T : ClassA, I1 | T 同时满足多个约束条件 |
示例代码:
// 要求 T 必须是可空类型,并且可比较
public class MyComparer<T> where T : struct, IComparable<T> {
public bool Compare(T a, T b) {
return a.CompareTo(b) == 0;
}
}
3.3 约束的比喻
约束如同“快递箱的尺寸限制”:
- 如果规定箱子只能装 A4 纸(类型约束),则不能放入超过尺寸的物体。
- 要求箱子必须防水(接口约束),则所有放入的物品必须满足这一条件。
四、协变与逆变(Covariance & Contravariance)
4.1 基本概念
协变和逆变是泛型类型间兼容性的扩展规则,允许在特定场景下使用更灵活的类型转换。
协变(Covariance)
允许将 IEnumerable<Derived>
转换为 IEnumerable<Base>
,即“子类型到父类型”的转换。例如:
IEnumerable<Animal> animals = new List<Dog>(); // 允许
逆变(Contravariance)
允许将 Action<Base>
转换为 Action<Derived>
,即“父类型到子类型”的转换。例如:
Action<Animal> feedAnimal = (a) => { /* ... */ };
Action<Dog> feedDog = feedAnimal; // 允许
4.2 泛型接口与委托的标记
要启用协变或逆变,需在接口或委托定义时添加 out
(协变)或 in
(逆变)修饰符。例如:
// 协变接口
public interface IEnumerable<out T> { /* ... */ }
// 逆变委托
public delegate void Action<in T>(T item);
五、泛型的高级技巧与最佳实践
5.1 通过反射动态创建泛型类型
// 动态创建泛型列表
Type genericListType = typeof(List<>).MakeGenericType(typeof(string));
var stringList = Activator.CreateInstance(genericListType);
5.2 泛型与性能优化
由于泛型在编译时生成专用代码,避免了 object
的装箱开销。例如:
// 非泛型版本
public static void AddNumbers(List<object> list) {
int sum = 0;
foreach (var item in list) {
sum += (int)item; // 每次都需要拆箱
}
}
// 泛型版本
public static void AddNumbers<T>(List<T> list) where T : struct, IConvertible {
int sum = 0;
foreach (var item in list) {
sum += Convert.ToInt32(item); // 直接转换,无需装箱
}
}
5.3 泛型与代码复用
通过泛型可减少重复代码。例如,实现一个通用的“缓存”类:
public class GenericCache<TKey, TValue> {
private Dictionary<TKey, TValue> _cache = new();
public void Add(TKey key, TValue value) => _cache[key] = value;
public TValue Get(TKey key) => _cache[key];
}
六、常见问题与解决方案
6.1 泛型类型参数的命名规范
通常使用单字母命名(如 T
、U
、K
、V
),也可根据语义命名(如 TProduct
)。
6.2 泛型与多态的结合
// 泛型方法与基类的结合
public void ProcessItems<T>(IEnumerable<T> items) where T : BaseClass {
foreach (T item in items) {
item.VirtualMethod(); // 调用子类重写的方法
}
}
6.3 泛型与继承的注意事项
- 泛型类本身不能继承非泛型类。
- 泛型类的约束需继承自指定基类或接口。
结论
C# 泛型通过类型参数化实现了代码的复用性、类型安全性和高效性。从基础的集合类到复杂的通用算法,泛型贯穿于整个 C# 生态系统。掌握泛型的约束、协变逆变以及高级技巧,能让开发者编写出更优雅、健壮的代码。
在实际项目中,建议优先使用内置泛型集合类(如 List<T>
、Dictionary<TKey, TValue>
),并根据需求自定义泛型工具类。通过持续实践,泛型将成为你应对复杂开发场景的得力工具。
(全文约 1800 字)