跳到主要内容

2.5 泛型、委托、事件与模式匹配

泛型、委托、事件和模式匹配是 C# 从“能写流程代码”走向“能组织业务抽象”的关键能力。它们经常一起出现:泛型定义可复用结构,委托传入可替换行为,事件通知状态变化,模式匹配让复杂分支保持可读。

学习目标

  • 能定义和使用泛型类型、泛型方法和泛型约束。
  • 能理解委托、FuncAction 和 Lambda 表达式的关系。
  • 能用事件表达对象状态变化,而不是让调用方反复轮询。
  • 能使用模式匹配替代难维护的类型判断和条件嵌套。

应用场景

  • 在服务层中复用分页结果、查询条件和校验规则。
  • 给集合筛选、排序、重试或转换逻辑传入一段可替换行为。
  • 让领域对象在任务完成、订单支付或库存变更时发出通知。
  • 根据请求类型、状态枚举、对象形状选择不同业务分支。

核心概念

概念说明
泛型使用类型参数编写可复用代码,例如 List<T>Result<T>IRepository<TEntity>
泛型约束限定类型参数必须满足的条件,例如 where T : classwhere 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 nullvalue is not null
  • 丢弃模式:_ 表示兜底分支。

重点难点

  • 泛型不是“任意类型都能用”,约束越清晰,方法内部能安全使用的成员越明确。
  • 委托代表可调用行为,不代表异步;异步委托通常返回 TaskTask<T>
  • 事件应该表达已经发生的事实,例如 CompletedPaidStockChanged,不要命名成命令式的 CompleteTodo
  • 事件处理器异常会影响发布方,生产代码需要明确是否捕获、记录或继续抛出。
  • 模式匹配分支按顺序匹配,越具体的分支应放在越前面。

常见误区

误区推荐做法
把所有类型都写成泛型只有多个类型共享同一套逻辑时再抽泛型
滥用 object 和强制转换使用泛型和约束保留类型安全
为每个 Lambda 都定义委托类型常规输入输出优先使用 FuncAction
在事件里写核心业务流程事件用于通知和扩展,主流程仍应保持显式
switch 分支先写兜底 __ 放最后,避免吞掉后续规则

练习

  • 定义一个 Result<T>,同时表示成功值和错误信息。
  • TodoItem 增加 DueDate,用模式匹配返回“逾期”“今日截止”“未来任务”。
  • 写一个 IRule<TodoItem> 实现,禁止完成标题为空的任务。
  • Completed 事件注册一个处理器,把完成记录写入日志。

延伸阅读