1:本篇学习目标
- 理解泛型的核心思想,掌握 C# 泛型的基本语法
- 彻底搞懂 C# 泛型与 C++ 模板的本质区别,消除认知误区
- 掌握泛型类、泛型方法的定义与使用
- 熟练运用 6 种泛型约束,理解每种约束的作用和适用场景
- 掌握
List<T>、Dictionary<TKey, TValue>两大核心泛型集合的常用操作 - 了解
Queue<T>、Stack<T>、HashSet<T>等常用泛型集合的用法 - 所有知识点均与 C++ 模板、STL 容器进行深度对比
2:泛型概述
泛型(Generics)是一种允许我们在定义类、方法、接口时不指定具体类型,而是在使用时再指定类型的技术。泛型的核心价值是类型安全 + 代码复用。
1:为什么需要泛型
在没有泛型的时代,如果我们想实现一个通用的集合,只能使用object类型:
public class ArrayList { private object[] _items; public void Add(object item) { ... } public object this[int index] { get { ... } } }这种方式有两个严重问题:
- 类型不安全:可以向集合中添加任何类型的对象,取出时需要强制类型转换,容易出现运行时异常
- 性能损耗:值类型存入时需要装箱,取出时需要拆箱,带来显著的性能开销
泛型完美解决了这两个问题:
public class List<T> { private T[] _items; public void Add(T item) { ... } public T this[int index] { get { ... } } }使用泛型后,List<int>只能存储int类型,List<string>只能存储string类型,编译时就能保证类型安全,也不需要装箱拆箱。
2:C#泛型与C++模板的本质区别
这是 C++ 开发者最容易误解的知识点,两者虽然语法相似,但底层实现机制完全不同:
| 对比维度 | C# 泛型 | C++ 模板 |
|---|---|---|
| 实例化时机 | 运行时由 CLR 完成 | 编译时由编译器完成 |
| 代码生成方式 | 所有引用类型共享同一份代码,值类型各自生成独立代码 | 每个类型都生成独立的代码(模板展开) |
| 类型检查 | 编译时对泛型约束进行检查,运行时保证类型安全 | 编译时对每个具体类型进行检查,错误信息晦涩 |
| 约束系统 | 有明确的where约束语法,约束类型的能力 | C++20 之前只有隐式约束,C++20 引入 Concepts |
| 泛型参数 | 只能是类型 | 可以是类型、整数、指针等 |
| 特化 | 不支持模板特化 | 支持完全特化和偏特化 |
核心结论:
- C++ 模板本质是代码生成器,编译时为每个类型生成一份独立的代码
- C# 泛型本质是运行时类型系统,由 CLR 在运行时处理,更轻量、更类型安全
3:泛型类
泛型类是最常用的泛型形式,在类名后使用尖括号<>包裹类型参数。
1:定义一个泛型类
// 定义一个泛型栈类 public class MyStack<T> { private T[] _items; private int _count; public MyStack(int capacity = 10) { _items = new T[capacity]; _count = 0; } public void Push(T item) { if (_count >= _items.Length) { Array.Resize(ref _items, _items.Length * 2); } _items[_count++] = item; } public T Pop() { if (_count == 0) { throw new InvalidOperationException("栈为空"); } T item = _items[--_count]; _items[_count] = default(T); // 清空引用,帮助GC回收 return item; } public T Peek() { if (_count == 0) { throw new InvalidOperationException("栈为空"); } return _items[_count - 1]; } public int Count => _count; }2:使用泛型类
// 使用int类型的栈 MyStack<int> intStack = new MyStack<int>(); intStack.Push(1); intStack.Push(2); intStack.Push(3); Console.WriteLine(intStack.Pop()); // 输出 3 Console.WriteLine(intStack.Peek()); // 输出 2 // 使用string类型的栈 MyStack<string> stringStack = new MyStack<string>(); stringStack.Push("Hello"); stringStack.Push("World"); Console.WriteLine(stringStack.Pop()); // 输出 World3:多个类型参数
泛型类可以有多个类型参数,用逗号分隔:
// 定义一个泛型键值对类 public class MyPair<TKey, TValue> { public TKey Key { get; set; } public TValue Value { get; set; } public MyPair(TKey key, TValue value) { Key = key; Value = value; } public override string ToString() { return $"[{Key}, {Value}]"; } } // 使用 MyPair<int, string> pair = new MyPair<int, string>(1, "张三"); Console.WriteLine(pair); // 输出 [1, 张三]4:泛型方法
方法也可以是泛型的,即使所在的类不是泛型类。
1:定义泛型方法
public static class ArrayUtils { // 泛型方法:交换数组中两个元素的位置 public static void Swap<T>(T[] array, int index1, int index2) { T temp = array[index1]; array[index1] = array[index2]; array[index2] = temp; } // 泛型方法:查找数组中元素的索引 public static int IndexOf<T>(T[] array, T item) { for (int i = 0; i < array.Length; i++) { if (EqualityComparer<T>.Default.Equals(array[i], item)) { return i; } } return -1; } }2:调用泛型方法
int[] numbers = { 1, 2, 3, 4, 5 }; ArrayUtils.Swap(numbers, 0, 4); // 现在数组变成 {5, 2, 3, 4, 1} int index = ArrayUtils.IndexOf(numbers, 3); Console.WriteLine(index); // 输出 2类型推断:调用泛型方法时,编译器通常可以根据参数自动推断出类型参数,不需要显式指定
ArrayUtils.Swap<int>(numbers, 0, 4); // 显式指定类型 ArrayUtils.Swap(numbers, 0, 4); // 编译器自动推断类型,推荐写法5:泛型约束
默认情况下,泛型类型参数T可以是任何类型,这意味着我们只能对T执行object类拥有的操作(如ToString()、Equals()等)。
如果我们需要对T执行更多操作,就需要使用泛型约束来限制类型参数的范围。C# 使用where关键字指定约束。
1:六种泛型约束
| 约束 | 作用 | 对应 C++ 概念 |
|---|---|---|
where T : struct | T 必须是值类型 | 类似std::is_arithmetic_v<T> |
where T : class | T 必须是引用类型 | 类似std::is_class_v<T> |
where T : new() | T 必须有公共的无参数构造函数 | 类似std::is_default_constructible_v<T> |
where T : 基类名 | T 必须继承自指定的基类 | 类似std::is_base_of_v<Base, T> |
where T : 接口名 | T 必须实现指定的接口 | 类似 Concepts 中的接口约束 |
where T : U | T 必须继承自另一个类型参数 U | 偏特化的一种形式 |
2:约束使用示例
值类型约束
public class NumberCalculator<T> where T : struct { public T Add(T a, T b) { // 注意:即使有struct约束,也不能直接使用+运算符 // 需要借助泛型数学或动态方法,C# 11引入了INumber<T>接口解决此问题 return (T)((dynamic)a + (dynamic)b); } }引用类型约束
public class ReferenceTypeContainer<T> where T : class { private T _item; public void SetItem(T item) { _item = item; } public bool IsNull() { return _item == null; // 有class约束才能用==null比较 } }构造函数约束
public class Factory<T> where T : new() { public T CreateInstance() { return new T(); // 有new()约束才能new T() } }基类约束
public class AnimalContainer<T> where T : Animal { private List<T> _animals = new List<T>(); public void Add(T animal) { _animals.Add(animal); } public void MakeAllSound() { foreach (T animal in _animals) { animal.MakeSound(); // 有基类约束才能调用基类方法 } } }接口约束
public class ComparableUtils<T> where T : IComparable<T> { public static T Max(T a, T b) { return a.CompareTo(b) > 0 ? a : b; // 有接口约束才能调用CompareTo } public static T Min(T a, T b) { return a.CompareTo(b) < 0 ? a : b; } }3:组合约束
可以将多个约束组合在一起使用:
public class MyContainer<T> where T : class, IDisposable, new() { // T必须是引用类型,实现IDisposable接口,且有公共无参构造函数 }注意:多个约束有顺序要求:
- 主约束(
class/struct/ 基类)必须放在最前面 - 接口约束放在中间
new()构造函数约束必须放在最后
6:总结
本来想将常用的泛型集合的,但是想了想还是单开一个专题,最后讲吧。
在这篇博客中,我们系统学习了 C# 的泛型系统和常用泛型集合。对于 C++ 开发者来说,核心是理解:
- C# 泛型是运行时类型系统,不是编译时代码展开
- 泛型约束是保证类型安全的关键,比 C++ 早期的隐式约束更清晰
- 泛型集合替代了早期的非泛型集合,既类型安全又高性能