C# 泛型详解:概念、泛型类、类型参数与约束
泛型编程允许开发者编写能够处理多种数据类型的通用代码,而无需为每种特定类型重复编写相同的逻辑。通过引入抽象层,泛型大大提高了代码的可重用性和可维护性。
C# 中的泛型
C# 语言通过泛型(Generics)提供了类型参数化的能力。这意味着可以编写代码模板,然后在创建具体类型实例时,为这些模板指定实际的数据类型。类型本身可以被看作是对象的蓝图,而泛型则可以看作是类型的蓝图。
C# 支持五种泛型:类、结构、接口、委托和方法。前四者是类型,而方法是成员。
泛型的引入
考虑一个简单的整数栈实现:
class IntStack
{
private int stackPointer = 0;
private int[] stackArray;
public void Push(int item) { /* ... implementation ... */ }
public int Pop() { /* ... implementation ... */ return default(int); }
}
如果需要一个浮点数栈,传统做法是复制代码并修改所有 `int` 为 `float`:
class FloatStack
{
private int stackPointer = 0;
private float[] stackArray;
public void Push(float item) { /* ... implementation ... */ }
public float Pop() { /* ... implementation ... */ return default(float); }
}
这种方法存在明显缺点:代码冗余,维护困难,每增加一种新类型(如 `long`, `double`)都需要重复劳动。
泛型解决了这个问题:
class StackGeneric<T>
{
private int stackPointer = 0;
private T[] stackArray;
public void Push(T item) { /* ... implementation ... */ }
public T Pop() { /* ... implementation ... */ return default(T); }
}
在 `StackGeneric
泛型类
创建和使用非泛型类通常只需两个步骤:声明类和实例化。泛型类本身是类型的模板,需要先由具体类型实例化,才能创建其实例。
- 声明泛型类型:使用类型占位符(类型参数)来声明类。
- 创建构造类型:为类型参数提供实际类型,生成具体的类定义,称为构造类型。
- 创建实例:实例化构造类型。
声明泛型类
泛型类的声明语法在普通类声明的基础上,在类名后添加一对尖括号 `<>`,其中包含用逗号分隔的类型参数。
class GenericContainer{ public TKey Key; public TValue Value; }
这里的 `TKey` 和 `TValue` 就是类型参数,它们在类体内用作占位符。
创建构造类型
通过为泛型类声明中的类型参数提供具体的类型实参,可以创建构造类型。
// 使用 int 和 string 作为类型实参,创建 GenericContainer 的构造类型 GenericContainerpairInstance;
编译器根据提供的类型实参,生成一个具体的类定义。
类型参数与类型实参
- 类型参数 (Type Parameter):在泛型类声明中定义的占位符,如 `TKey`, `TValue`。
- 类型实参 (Type Argument):在创建构造类型时,提供给类型参数的实际类型,如 `int`, `string`。
// T1, T2 是类型参数
class SampleGeneric<T1, T2> { /* ... */ }
// int, float 是类型实参
SampleGeneric<int, float> myGenericInstance;
创建变量和实例
创建泛型类(构造类型)的变量和实例,与创建非泛型类对象类似。
// 显式指定构造类型
GenericContainer<short, int> container1 = new GenericContainer<short, int>();
// 使用 var 关键字,编译器推断类型
var container2 = new GenericContainer<short, int>();
变量声明和实例创建也可以分开进行:
GenericContainer<short, int> myContainerRef; // 声明引用
myContainerRef = new GenericContainer<short, int>(); // 实例化并赋值
泛型类与非泛型类的比较
| 方面 | 非泛型类 | 泛型类 |
|---|---|---|
| 代码大小 | 每种类型都需要一个实现,代码量大 | 一个模板适用于所有构造类型,代码量小 |
| 可执行文件大小 | 编译时会包含所有实现的版本 | 只包含实际使用的构造类型 |
| 编写难度 | 简单,因具体而明确 | 稍复杂,因抽象而通用 |
| 维护难度 | 修改需应用到所有版本,易出错 | 修改只需一处,易于维护 |
类型参数的约束
有时,泛型代码需要对类型参数施加限制,以确保它们支持某些操作。例如,比较两个泛型类型的大小:
class Comparer<T>
{
// 错误示例:T 可能不支持 '<' 运算符
// public static bool IsLess(T a, T b)
// {
// return a < b;
// }
}
编译器无法保证类型参数 `T` 支持 `<` 运算符。此时需要使用约束来限定 `T` 必须是支持该操作的类型。
where 子句
约束通过 `where` 子句来指定。每个有约束的类型参数都可以有自己的 `where` 子句,多个约束用逗号分隔。
// where 子句语法
// TypeParam: 约束1, 约束2, ...
// 示例:
class DataProcessor<TData, TComparator>
where TData : struct // TData 必须是值类型
where TComparator : IComparer // TComparator 必须实现 IComparer 接口
{
// ...
}
要点:
- `where` 子句跟在类型参数列表之后。
- 约束列表内用逗号分隔。
- `where` 是上下文关键字。
约束的类型和顺序
约束共有五种类型:
- 类名约束:类型参数必须是指定的类或其派生类。
class约束:类型参数必须是引用类型。struct约束:类型参数必须是值类型。- 接口约束:类型参数必须实现指定的接口。
new()约束:类型参数必须有一个无参公共构造函数(称为构造函数约束)。
在 `where` 子句中,约束的顺序有要求:
- 最多一个主约束(类名、
struct或class),且必须放在最前面。 - 可以有任意数量的接口约束。
- 构造函数约束 (`new()`) 必须放在最后。