C# 中的泛型:为什么需要以及如何使用
泛型(Generics)是 C# 语言的关键特性之一,它允许创建带有参数化类型的类、方法和接口。无需为每种数据类型编写相同的代码,只需编写一次,在使用时指定类型即可。这提高了性能、类型安全性和代码的可重用性。在本文中,我们将分析为什么需要泛型,如何在实际中应用它们,并查看 .NET 中的真实示例。
为什么 C# 中需要泛型
在 C#(2.0 版本)出现泛型之前,开发人员经常使用 object 类型和 ArrayList 集合。这导致了两个问题:
- 性能损失 — 值类型(int, double, struct)的装箱(boxing)和拆箱(unboxing)。
- 缺少编译时类型检查 — 可能意外地将字符串放入数字列表,并仅在运行时才得到错误。
泛型解决了这两个问题。它们允许创建类型安全的数据结构,而无需额外的开销。例如,List<int> 处理 int 时无需装箱,并且编译器不允许向其中添加字符串。
泛型的基本概念
泛型类
泛型类通过在尖括号中声明类型参数来定义。类型参数是一个占位符,在创建实例时会被实际类型替换。
public class Box<T>{ private T _value;
public Box(T value) { _value = value; }
public T GetValue() => _value;}使用方式:
var intBox = new Box<int>(42);var stringBox = new Box<string>("Hello");
Console.WriteLine(intBox.GetValue()); // 42Console.WriteLine(stringBox.GetValue());// Hello泛型方法
方法可以拥有自己的类型参数,即使类本身不是泛型:
public class Utils{ public static void Swap<T>(ref T a, ref T b) { T temp = a; a = b; b = temp; }}
// 使用方式int x = 1, y = 2;Utils.Swap(ref x, ref y);Console.WriteLine($"{x} {y}"); // 2 1泛型接口
带有类型参数的接口允许创建灵活的抽象:
public interface IRepository<T>{ T GetById(int id); void Add(T entity);}
public class UserRepository : IRepository<User>{ public User GetById(int id) => new User(); public void Add(User user) { }}泛型中的约束(Constraints)
有时需要对可以替换的类型施加限制。为此,使用关键字 where。
主要的约束类型
where T : class— 仅限引用类型(string, 类)。where T : struct— 仅限值类型(int, DateTime, struct)。where T : new()— 类型必须具有公共的无参数构造函数。where T : SomeBaseClass— 类型必须是或继承自指定的类。where T : ISomeInterface— 类型必须实现该接口。
具有多个约束的示例:
public class Repository<T> where T : class, IEntity, new(){ public T Create() { return new T(); // 由于 new() 约束而允许 }}泛型中的协变和逆变
从 C# 4.0 开始,可以使用关键字 in 和 out 来控制泛型接口的变体性。
- 协变(out) — 允许使用