C#
委托、事件、Lambda 与扩展成员
掌握委托、Lambda、事件生命周期及 C# 14 扩展块,安全组合行为。
发布于 2026年7月23日
委托、事件、Lambda 与扩展成员
委托把“可以调用的行为”变成类型安全的值;Lambda 提供紧凑实现;事件在发布者和订阅者之间建立通知;扩展成员则为无法修改的类型补充调用表面。这些工具都在组合行为,但生命周期和隐藏依赖必须清楚。
一、学习目标
- 声明和调用自定义委托
- 使用
Action、Func与 Lambda - 理解闭包捕获和委托组合
- 正确发布、订阅和取消订阅事件
- 编写传统扩展方法与 C# 14 扩展块
- 避免把关键业务规则藏进回调
二、委托是类型安全的调用契约
public delegate bool TaskPredicate(StudyTask task);
static IReadOnlyList<StudyTask> Filter(
IEnumerable<StudyTask> source,
TaskPredicate predicate)
{
var result = new List<StudyTask>();
foreach (StudyTask task in source)
{
if (predicate(task))
{
result.Add(task);
}
}
return result;
}
调用:
IReadOnlyList<StudyTask> pending = Filter(
tasks,
task => task.Status == TaskStatus.Todo);
委托实例包含目标方法;实例方法委托还持有目标对象引用。长期保存委托时,要考虑它是否让本应回收的对象继续存活。
三、Action 与 Func
常用签名无需自定义委托:
Action<string> writeLine = Console.WriteLine;
Func<StudyTask, bool> isPending =
task => task.Status == TaskStatus.Todo;
Func<StudyTask, string> selectTitle =
task => task.Title;
Action 无返回值,Func 的最后一个类型参数是返回值。若委托代表重要领域概念,或参数含义仅靠类型看不清,自定义命名委托仍更易读。
副作用回调要明确失败策略。多播委托中一个处理器抛异常时,后续处理器不会自动执行;不要把它当作可靠消息队列。
四、Lambda 与闭包
Lambda 可以捕获外层变量:
TaskStatus expected = TaskStatus.Todo;
Func<StudyTask, bool> matches =
task => task.Status == expected;
捕获的是变量而不是声明瞬间的值:
expected = TaskStatus.Done;
bool result = matches(task); // 使用新的 expected
捕获可能生成额外对象,并延长外部对象生命周期。高频路径先测量;更重要的是避免回调依赖随时变化的隐藏状态。
无捕获 Lambda 可标记 static:
Func<StudyTask, string> titleSelector =
static task => task.Title;
编译器会阻止它读取局部变量,让依赖更显式。
五、事件用于通知
public sealed class TaskCompletedEventArgs(
StudyTask task) : EventArgs
{
public StudyTask Task { get; } = task;
}
public sealed class TaskService
{
public event EventHandler<TaskCompletedEventArgs>? TaskCompleted;
private void OnTaskCompleted(StudyTask task)
{
TaskCompleted?.Invoke(
this,
new TaskCompletedEventArgs(task));
}
}
event 限制外部只能订阅和取消订阅,不能直接触发或替换委托。发布者决定何时通知。
订阅:
service.TaskCompleted += HandleCompleted;
try
{
service.Complete(id);
}
finally
{
service.TaskCompleted -= HandleCompleted;
}
长生命周期发布者会持有订阅者引用。不再需要通知时必须取消订阅,或使用与发布者相同的受控生命周期。
六、事件不是事务消息
进程内事件适合 UI 更新、指标或弱耦合通知,但不保证:
- 进程崩溃后重放
- 跨服务传递
- 每个处理器都成功
- 与数据库提交原子一致
如果“任务完成后必须同步远端”是业务要求,应把它纳入用例事务或可靠 outbox,而不是只挂一个事件处理器。
发布事件时还要决定处理器异常策略。默认直接传播最诚实;若要隔离处理器,逐个调用并收集失败,同时记录清晰结果。
七、传统扩展方法
public static class TaskExtensions
{
public static string ToDisplayLabel(this StudyTask task)
{
ArgumentNullException.ThrowIfNull(task);
string marker = task.Status == TaskStatus.Done ? "[x]" : "[ ]";
return $"{marker} {task.Id}: {task.Title}";
}
}
导入命名空间后:
Console.WriteLine(task.ToDisplayLabel());
扩展方法是静态方法的调用语法糖,不能访问类型私有成员,也不能真正修改原类型。实例成员与扩展成员同名时,实例成员优先。
八、C# 14 扩展块
C# 14 可以在顶层、非泛型静态类中声明扩展块:
public static class StudyTaskExtensions
{
extension(StudyTask task)
{
public string DisplayLabel
{
get
{
string marker =
task.Status == TaskStatus.Done ? "[x]" : "[ ]";
return $"{marker} {task.Id}: {task.Title}";
}
}
public bool IsOlderThan(
TimeProvider timeProvider,
TimeSpan age) =>
timeProvider.GetUtcNow() - task.CreatedAt > age;
}
}
调用:
Console.WriteLine(task.DisplayLabel);
bool stale = task.IsOlderThan(timeProvider, TimeSpan.FromDays(30));
扩展块可以定义方法、属性和运算符,也可以带泛型接收者:
public static class SequenceExtensions
{
extension<T>(IEnumerable<T> source)
{
public bool IsEmpty => !source.Any();
}
}
扩展成员应补充局部、通用能力。若类型源码属于自己且能力是核心语义,优先修改类型本身。
九、行为参数与策略
public static IReadOnlyList<StudyTask> SortTasks(
IEnumerable<StudyTask> tasks,
Comparison<StudyTask> comparison)
{
var result = tasks.ToList();
result.Sort(comparison);
return result;
}
调用方选择策略:
var sorted = SortTasks(
tasks,
static (left, right) =>
left.CreatedAt.CompareTo(right.CreatedAt));
这种方式适合小型纯策略。策略需要配置、日志或多个方法时,建立命名接口和实现类型会比传递一组相关委托清楚。
十、常见错误
在循环中意外捕获可变变量
确认每个回调捕获的是独立局部值,并用测试验证延迟执行结果。
订阅后从不取消
长生命周期发布者会让订阅者无法回收,还可能重复处理通知。
用事件保证关键业务
普通内存事件没有持久性和交付保证。关键副作用需要显式可靠机制。
用扩展成员隐藏复杂 I/O
task.LoadRemoteState() 看似普通实例调用,却可能访问网络。扩展成员应保持语义透明。
十一、练习与自测
练习:
- 用
Func<StudyTask,bool>实现可组合筛选器。 - 用静态 Lambda 消除一次不必要捕获。
- 添加任务完成事件,并证明取消订阅后不再收到通知。
- 用 C# 14 扩展块实现
DisplayLabel和IsEmpty。
自测:
- 委托实例可能持有哪些对象引用?
- 多播委托中处理器抛异常会发生什么?
- event 比公开委托字段多了什么限制?
- 扩展成员和实例成员同名时调用哪个?
十二、官方资料
上一篇:可空引用、异常与资源安全 | 下一篇:LINQ、迭代器与惰性查询