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 { ... } }
}

这种方式有两个严重问题:

  1. 类型不安全:可以向集合中添加任何类型的对象,取出时需要强制类型转换,容易出现运行时异常
  2. 性能损耗:值类型存入时需要装箱,取出时需要拆箱,带来显著的性能开销

泛型完美解决了这两个问题:

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()); // 输出 World

3:多个类型参数

泛型类可以有多个类型参数,用逗号分隔:

// 定义一个泛型键值对类
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接口,且有公共无参构造函数
}

注意: 多个约束有顺序要求:

  1. 主约束(class/struct/ 基类)必须放在最前面
  2. 接口约束放在中间
  3. new()构造函数约束必须放在最后

6:总结

本来想将常用的泛型集合的,但是想了想还是单开一个专题,最后讲吧。

在这篇博客中,我们系统学习了 C# 的泛型系统和常用泛型集合。对于 C++ 开发者来说,核心是理解:

  1. C# 泛型是运行时类型系统,不是编译时代码展开
  2. 泛型约束是保证类型安全的关键,比 C++ 早期的隐式约束更清晰
  3. 泛型集合替代了早期的非泛型集合,既类型安全又高性能

更多推荐