订单系统里有两类“以后再调用”的代码。折扣计算必须立刻返回一个价格,适合传入一个策略;订单状态变化则要同时通知审计、界面和指标模块,却不关心它们是否存在。前者是委托的舞台,后者是事件的边界。
本章用 OrderProcessor 贯穿全程:先把定价行为作为类型安全回调传入,再用事件发布订单状态变化,最后解决多播返回值、Lambda 捕获、退订泄漏和异步处理器等工程问题。
学习路线:委托契约 → 内置委托与 Lambda → 多播调用链 → 事件封装 → 标准事件模式 → 生命周期与异步边界。

委托类型描述“可以调用什么”,包括参数数量、参数类型和返回类型。方法名、所属类可以不同,只要签名兼容,就能成为同一委托的目标。
public delegate decimal PricingStrategy(decimal subtotal, decimal rate);
static decimal ApplyDiscount(decimal subtotal, decimal rate)
=> subtotal * (1 - rate);
static decimal AddServiceFee(decimal subtotal, decimal rate)
=> subtotal * (1 + rate);PricingStrategy strategy = ApplyDiscount;
Console.WriteLine(strategy(100m, 0.15m));
strategy = AddServiceFee;
Console.WriteLine(strategy(100m, 0.15m));输出:
85.00
115.00这里发生了方法组到委托的转换。编译器会阻止把 (string) → void 的方法塞进 (decimal, decimal) → decimal 的插槽,因此回调错误能在运行前暴露。调用委托前若它可能为 null,可用 strategy?.Invoke(...);若回调是必需依赖,更好的做法是在构造时校验并保持非空。
多数场景不需要自定义委托声明,标准泛型委托已经覆盖常见形状:
Action<string> audit = message => Console.WriteLine($"AUDIT {message}");
Func<decimal, decimal, decimal> discount =
(subtotal, rate) => subtotal * (1 - rate);
Predicate<Order> isPriority =
AUDIT order-created
180.0
TruePredicate<T> 与 Func<T, bool> 形状相似,但某些 API 明确要求前者。公开 API 若需要表达一个有领域含义、会被广泛复用的回调,也可以声明自定义委托;仅为内部策略传递时,Func/Action 通常更简洁。
Lambda 是创建委托目标的表达式。表达式 Lambda 适合单一计算,语句 Lambda 则可以包含多条语句:
Func<decimal, decimal> addTax = price => price * 1.06m;
Action<Order> print = order =>
{
Console.WriteLine($"Order={order.Id}");
Console.WriteLine($"Total={order.Total:C
Lambda 引用外部局部变量时会形成闭包。它捕获的是变量本身,而不是创建委托那一刻的值:
decimal rate = 0.10m;
Func<decimal, decimal> price = amount => amount * (1 - rate);
rate = 0.20m;
Console.WriteLine(price(100m));80.00闭包对象会延长被捕获状态的生命周期。不要无意间捕获大对象、界面组件或请求作用域服务;并发调用共享闭包时,还要考虑状态竞争。循环中需要“固定本轮值”时,可在循环体内复制到新的局部变量再捕获。

委托可以用 += 组合,用 -= 移除。调用时按调用列表顺序同步执行:
Action<Order> pipeline = order => Console.WriteLine("A audit");
pipeline += order => Console.WriteLine("B ui");
pipeline += order => Console.WriteLine("C metrics");
pipeline(order);A audit
B ui
C metrics有返回值的多播委托会调用全部目标,却只把最后一个目标的返回值交给调用方:
Func<int> values = () => 10;
values += () => 20;
Console.WriteLine(values());输出是 20。若每个结果都重要,应通过 GetInvocationList() 显式遍历、转换并收集,而不是依赖多播返回值。
一个目标抛异常时,普通调用会立即停止,后续目标不会执行。是否隔离异常是发布者的策略决定;若选择逐个隔离,就必须明确记录失败并决定最终如何汇总,不能无声吞掉。
触发前复制委托引用可得到不可变调用快照。当前触发过程中发生的订阅变化只影响下一次触发,控制流更容易推理。

公开委托字段会让外部代码覆盖整个调用列表,甚至主动触发通知。event 在委托之上增加访问限制:外部通常只能 += 和 -=,只有声明事件的类型能调用它。
public sealed class OrderProcessor
{
public event Action<Order>? OrderSubmitted;
public void Submit(Order order)
{
Save(order);
OrderSubmitted?.Invoke(order);
}
}processor.OrderSubmitted += order =>
Console.WriteLine($"submitted:{order.Id}");
processor.Submit(order);事件表达的是“已经发生的事实”,命名常用过去式或变化语义,例如 OrderSubmitted、StatusChanged。如果调用者需要返回值来决定发布者下一步怎么办,委托策略或显式方法往往比事件更合适。

.NET 常见事件签名是 EventHandler<TEventArgs>。sender 指向发布者,TEventArgs 携带本次通知的不可变数据。
public sealed class OrderStatusChangedEventArgs : EventArgs
{
public OrderStatusChangedEventArgs(
string oldStatus, string newStatus, DateTimeOffset occurredAt)
{
OldStatus = oldStatus;
NewStatus = newStatus;
OccurredAt = occurredAt;
}
public string OldStatus { get; }
public string NewStatus
public event EventHandler<OrderStatusChangedEventArgs>? StatusChanged;
private void OnStatusChanged(OrderStatusChangedEventArgs args)
=> StatusChanged?.Invoke(this, args);processor.StatusChanged += (_, args) =>
Console.WriteLine($"{args.OldStatus}->{args.NewStatus}");Created->Submitted把触发集中在 protected virtual OnXxx 或私有方法里,能统一不变量和测试入口。事件参数应描述触发时的事实,尽量不可变;不要把发布者内部可变集合直接暴露给订阅者。
发布者的调用列表强引用处理器目标。若应用级发布者活得很久,页面级订阅者即使离开界面,也可能因为仍被事件引用而无法回收。
具名处理器最容易正确退订:
void OnStatusChanged(object? sender, OrderStatusChangedEventArgs args)
=> Render(args.NewStatus);
processor.StatusChanged += OnStatusChanged;
// 生命周期结束时
processor.StatusChanged -= OnStatusChanged;下面的写法无法移除原来的 Lambda,因为第二个表达式创建了不同的委托实例:
processor.StatusChanged += (_, args) => Render(args.NewStatus);
processor.StatusChanged -= (_, args) => Render(args.NewStatus); // 无效可以把 Lambda 存进变量,或者返回 IDisposable 订阅令牌,在 Dispose() 中执行 -=。这样使用方能用 using 把订阅生命周期绑定到作用域。

EventHandler 返回 void,因此 async Lambda 会变成 async void。发布者无法等待它、收集结果或通过返回的 Task 观察异常:
processor.StatusChanged += async (_, args) =>
{
await SendNotificationAsync(args);
};这种模式只适合真正的“发出即忘记”,且处理器内部必须建立自己的异常边界。若发布者需要知道所有异步订阅者何时完成,定义显式异步回调更清楚:
private readonly List<Func<OrderStatusChangedEventArgs, Task>> _handlers = [];
public async Task PublishAsync(OrderStatusChangedEventArgs args)
{
Task[] tasks = _handlers.Select(handler => handler(args)).ToArray();
await Task.WhenAll(tasks);
}还要明确并行还是顺序、一个失败是否取消其他处理器、异常如何汇总,以及是否传递 CancellationToken。这些是协议的一部分,不能靠偶然行为决定。
订单折扣、重试判定、数据映射属于委托策略;订单已提交、温度已变化、按钮被点击属于事件。若通知必须可靠送达、持久化、跨进程重试,进程内事件并不够,应使用队列或消息总线。
不要为了“解耦”而把所有方法调用改成事件。事件会隐藏调用链,增加生命周期、顺序和异常策略的复杂度;只有一对多通知且触发权应受保护时,它才真正有价值。
请实现以下能力:
OrderProcessor 构造函数接收 Func<decimal, decimal, decimal> 定价策略。EventHandler<OrderStatusChangedEventArgs>。IDisposable 令牌保证页面离开后退订。Task 的协议并测试异常汇总。测试时同时断言调用顺序、调用次数、返回值、订阅变化的生效时机,以及释放订阅后处理器不再执行。闭包用例还应验证外部变量修改后的行为。
event 与不可变事件数据。IDisposable 令牌,异步通知则用可等待协议。委托让行为可以被安全传递;事件让变化可以被受控发布。真正成熟的设计不仅能“回调成功”,还清楚谁拥有触发权、何时算完成、失败如何传播,以及引用何时释放。