Skip to content

C# - 泛型

泛型允许你设计和编写类型安全而又灵活的类、方法和接口。它们让你能够为数据类型定义一个占位符(一个“类型参数”),然后在代码使用时指定这个类型。这允许你编写一段代码,它能与任何数据类型协同工作,从而最大化代码重用、性能和类型安全性。

最常见的例子是泛型集合。你无需创建单独的 IntList、StringList 和 ProductList,而是可以使用泛型 List<T>,其中 T 是你提供的类型参数:List<int>、List<string>、List<Product>。

泛型类使用尖括号(<>)中的类型参数进行定义。这允许类针对不同的数据类型进行实例化,同时确保类内部的所有操作都是类型安全的。

示例:一个简单的泛型“包装器”类

Section titled “示例:一个简单的泛型“包装器”类”

让我们创建一个可以“包装”任何类型值的简单类。

using System;
// 一个带有一个类型参数 T 的泛型类
public class DataWrapper<T>
{
// 内部数据是 T 类型
public T Data { get; private set; }
public DataWrapper(T data)
{
this.Data = data;
}
public void DisplayData()
{
Console.WriteLine($"Data is: {Data} (Type: {typeof(T).Name})");
}
}
// --- 用法 ---
// 为整数创建一个包装器
var intWrapper = new DataWrapper<int>(123);
intWrapper.DisplayData(); // 输出: Data is: 123 (Type: Int32)
// 为字符串创建一个包装器
var stringWrapper = new DataWrapper<string>("Hello, Generics!");
stringWrapper.DisplayData(); // 输出: Data is: Hello, Generics! (Type: String)

虽然创建自己的泛型类很强大,但你几乎总是应该优先选择 System.Collections.Generic 命名空间中高度优化的内置泛型集合。这些集合包括:

  • List<T>:最常见的,一个动态大小的列表。
  • Dictionary<TKey, TValue>:键值对的集合。
  • Queue<T>:先进先出(FIFO)集合。
  • Stack<T>:后进先出(LIFO)集合。
  • HashSet<T>:唯一元素的集合。

你也可以创建泛型方法,即使它们在非泛型类中。这对于对任何类型执行操作的实用函数非常有用。

这个经典的例子展示了一个可以交换任何类型两个变量的方法。

public class Utility
{
public static void Swap<T>(ref T a, ref T b)
{
T temp = a;
a = b;
b = temp;
}
}
// --- 用法 ---
int x = 10, y = 20;
Console.WriteLine($"Before: x={x}, y={y}");
Utility.Swap(ref x, ref y); // 类型 T 被推断为 int
Console.WriteLine($"After: x={x}, y={y}");
string s1 = "Hello", s2 = "World";
Console.WriteLine($"Before: s1={s1}, s2={s2}");
Utility.Swap(ref s1, ref s2); // 类型 T 被推断为 string
Console.WriteLine($"After: s1={s1}, s2={s2}");

在 .NET 引入内置泛型委托之前,你必须为每种不同的方法签名定义一个新的委托。现在,你几乎在所有情况下都可以使用 Func<...> 和 Action<...>。

  • Action<T>:表示一个带有一个或多个参数(最多 16 个)且返回 void 的方法。
  • Func<T, TResult>:表示一个带有一个或多个参数并返回 TResult 类型值的方法(最后一个类型参数始终是返回类型)。
// 这个 Func 委托表示任何接收两个 int 并返回一个 int 的方法。
Func<int, int, int> operation;
// 将其指向一个 'Add' 方法
operation = (a, b) => a + b; // 使用 lambda 表达式
Console.WriteLine($"Sum: {operation(10, 5)}"); // 输出: Sum: 15
// 将其指向一个 'Multiply' 方法
operation = (a, b) => a * b;
Console.WriteLine($"Product: {operation(10, 5)}"); // 输出: Product: 50

接口也可以是泛型的。这在现代软件设计中非常常见,例如在 Repository Pattern(仓储模式)中。

这个接口定义了一个数据仓储的契约,可以与任何实体类型协同工作。

// 泛型接口
public interface IRepository<T>
{
T GetById(int id);
IEnumerable<T> GetAll();
void Add(T entity);
}
// 'Product' 类的一个具体实现
public class ProductRepository : IRepository<Product>
{
private readonly List<Product> _products = new();
public void Add(Product entity) => _products.Add(entity);
public IEnumerable<Product> GetAll() => _products;
public Product GetById(int id) => _products.FirstOrDefault(p => p.Id == id);
}
// (假设 Product 类已存在)
public record Product(int Id, string Name);

有时你需要保证类型参数 T 具有某些能力(例如,它有一个无参数构造函数或实现了特定的接口)。你可以使用 where 关键字通过约束来强制实现这一点。

// 此方法只接受是类且具有无参数构造函数的类型 T。
public T CreateInstance<T>() where T : class, new()
{
return new T();
}
// 此方法要求 T 必须实现 IDisposable 接口。
public void ProcessAndDispose<T>(T item) where T : IDisposable
{
// 现在我们可以安全地调用 Dispose(),因为约束保证了它的存在。
item.Dispose();
}