2.5 泛型、委托、事件与模式匹配
泛型、委托、事件和模式匹配是 C# 从“能写流程代码”走向“能组织业务抽象”的关键能力。它们经常一起出现:泛型定义可复用结构,委托传入可替换行为,事件通知状态变化,模式匹配让复杂分支保持可读。
学习目标
- 能定义和使用泛型类型、泛型方法和泛型约束。
- 能理解委托、
Func、Action和 Lambda 表达式的关系。 - 能用事件表达对象状态变化,而不是让调用方反复轮询。
- 能使用模式匹配替代难维护的类型判断和条件嵌套。
应用场景
- 在服务层中复用分页结果、查询条件和校验规则。
- 给集合筛选、排序、重试或转换逻辑传入一段可替换行为。
- 让领域对象在任务完成、订单支付或库存变更时发出通知。
- 根据请求类型、状态枚举、对象形状选择不同业务分支。
核心概念
| 概念 | 说明 |
|---|---|
| 泛型 | 使用类型参数编写可复用代码,例如 List<T>、Result<T>、IRepository<TEntity> |
| 泛型约束 | 限定类型参数必须满足的条件,例如 where T : class 或 where T : IEntity |
| 委托 | 表示“可以被调用的方法签名”,常见形式是 Func<T, TResult> 和 Action<T> |
| 事件 | 基于委托的发布订阅机制,用于通知外部某件事已经发生 |
| 模式匹配 | 根据类型、属性、常量、关系或组合条件匹配分支 |
案例:任务规则与完成事件
下面的示例把四个概念放在同一个业务场景中:任务完成前先执行一组泛型规则,完成后发布事件,并用模式匹配决定展示文案。
public interface IRule<in T>
{
string Name { get; }
bool IsSatisfiedBy(T value);
}
public sealed class TodoItem
{
private readonly List<IRule<TodoItem>> rules = [];
public TodoItem(int id, string title, TodoPriority priority)
{
Id = id;
Title = title;
Priority = priority;
}
public event EventHandler<TodoCompletedEventArgs>? Completed;
public int Id { get; }
public string Title { get; }
public TodoPriority Priority { get; }
public bool IsCompleted { get; private set; }
public void AddRule(IRule<TodoItem> rule) => rules.Add(rule);
public void Complete(string completedBy)
{
var failedRule = rules.FirstOrDefault(rule => !rule.IsSatisfiedBy(this));
if (failedRule is not null)
{
throw new InvalidOperationException($"任务未满足规则:{failedRule.Name}");
}
IsCompleted = true;
Completed?.Invoke(this, new TodoCompletedEventArgs(Id, completedBy));
}
}
public sealed record TodoCompletedEventArgs(int TodoId, string CompletedBy);
public enum TodoPriority
{
Low,
Normal,
High
}
IRule<in T> 是一个泛型接口。in 表示逆变,适合“只消费 T、不返回 T”的接口;初学阶段可以先记住它常用于校验器、比较器、处理器这类输入型抽象。
示例:委托与 Lambda
委托可以把“怎么判断”从“遍历流程”里抽出来。下面的方法不关心具体筛选条件,只要求调用方传入一个返回 bool 的函数。
public static IReadOnlyList<TodoItem> FilterTodos(
IEnumerable<TodoItem> todos,
Func<TodoItem, bool> predicate)
{
return todos.Where(predicate).ToList();
}
var importantTodos = FilterTodos(
todos,
todo => todo.Priority is TodoPriority.High && !todo.IsCompleted);
如果一段逻辑需要返回值,优先考虑 Func<T, TResult>;如果只是执行动作、不返回结果,优先考虑 Action<T>。业务代码中不要为了“高级”而自定义大量委托类型,只有签名含义需要命名时才值得单独定义。
示例:模式匹配
模式匹配适合把复杂条件写成可扫描的业务规则。它比多层 if 更容易看出每个分支的边界。
public static string GetTodoBadge(TodoItem todo)
{
return todo switch
{
{ IsCompleted: true } => "已完成",
{ Priority: TodoPriority.High } => "高优先级",
{ Title.Length: > 20 } => "标题较长",
_ => "普通任务"
};
}
常见模式包括:
- 类型模式:
value is TodoItem todo。 - 属性模式:
{ Priority: TodoPriority.High }。 - 关系模式:
score is >= 60 and < 90。 - 空值模式:
value is null和value is not null。 - 丢弃模式:
_表示兜底分支。
重点难点
- 泛型不是“任意类型都能用”,约束越清晰,方法内部能安全使用的成员越明确。
- 委托代表可调用行为,不代表异步;异步委托通常返回
Task或Task<T>。 - 事件应该表达已经发生的事实,例如
Completed、Paid、StockChanged,不要命名成命令式的CompleteTodo。 - 事件处理器异常会影响发布方,生产代码需要明确是否捕获、记录或继续抛出。
- 模式匹配分支按顺序匹配,越具体的分支应放在越前面。
常见误区
| 误区 | 推荐做法 |
|---|---|
| 把所有类型都写成泛型 | 只有多个类型共享同一套逻辑时再抽泛型 |
滥用 object 和强制转换 | 使用泛型和约束保留类型安全 |
| 为每个 Lambda 都定义委托类型 | 常规输入输出优先使用 Func 和 Action |
| 在事件里写核心业务流程 | 事件用于通知和扩展,主流程仍应保持显式 |
switch 分支先写兜底 _ | 把 _ 放最后,避免吞掉后续规则 |
练习
- 定义一个
Result<T>,同时表示成功值和错误信息。 - 给
TodoItem增加DueDate,用模式匹配返回“逾期”“今日截止”“未来任务”。 - 写一个
IRule<TodoItem>实现,禁止完成标题为空的任务。 - 给
Completed事件注册一个处理器,把完成记录写入日志。
延伸阅读
- Microsoft Learn:泛型
- Microsoft Learn:委托和事件
- Microsoft Learn:模式匹配
- 本手册:集合与 LINQ 和 异常处理与 Result 模式