C# 中的 Event:让对象学会"开口说话"

在软件世界里,对象之间的沟通是个永恒的难题。你怎么让一个订单服务在完成下单后,同时通知邮件系统、库存系统,甚至日志系统------却又不让它们彼此紧紧绑死?C# 的 event 机制,就是为这个问题量身打造的答案。它的底层逻辑是发布-订阅模式:一个对象喊一声,所有关心这件事的人都能听到,而喊话的人根本不需要知道谁在听。


🧱 从委托说起,事件的根在这里

要理解 event,绕不开委托(Delegate) 。委托是 C# 里一种类型安全的"函数指针"------它描述的不是数据,而是一种方法的形状:接受什么参数、返回什么类型。

csharp 复制代码
// 定义一个委托类型:接受一个 string,无返回值
public delegate void NotifyHandler(string message);

有了委托,你就能把方法像变量一样传来传去。但裸委托有个危险:外部代码可以直接调用它,甚至用 = 一刀把所有订阅者清空。event 关键字的存在,就是给委托套上一层保护壳------外部只能 += 订阅或 -= 退订,触发的权力永远留在发布者手里

csharp 复制代码
public event NotifyHandler OnNotify;
// 外部能做的只有:OnNotify += xxx 或 OnNotify -= xxx
// 想直接 OnNotify(...) ?对不起,编译器不答应

📐 .NET 的标准姿势:EventHandler<T>

自己定义委托当然可以,但 .NET 早就备好了两个通用委托,覆盖了绝大多数场景:

委托类型 签名 适用场景
EventHandler (object sender, EventArgs e) 事件不需要携带额外数据
EventHandler<T> (object sender, T e) 事件需要携带自定义数据

其中 sender 是触发事件的对象本身,e 是事件数据。这个约定在整个 .NET 生态里高度统一,跟着走准没错。

当事件需要携带数据时,继承 EventArgs 写一个数据类:

csharp 复制代码
public class OrderEventArgs : EventArgs
{
    public int OrderId { get; }
    public decimal Amount { get; }

    public OrderEventArgs(int orderId, decimal amount)
    {
        OrderId = orderId;
        Amount = amount;
    }
}

🔍 完整示例:一个订单,三方响应

用电商下单场景把所有概念串起来。一笔订单成交后,邮件服务要发确认邮件,库存服务要锁库存------两件事同时发生,互不干扰。

csharp 复制代码
using System;

// ── 事件数据类 ──────────────────────────────────
public class OrderEventArgs : EventArgs
{
    public int OrderId { get; }
    public decimal Amount { get; }

    public OrderEventArgs(int orderId, decimal amount)
    {
        OrderId = orderId;
        Amount = amount;
    }
}

// ── 发布者:订单服务 ────────────────────────────
public class OrderService
{
    // 声明事件
    public event EventHandler<OrderEventArgs>? OrderPlaced;
    public event EventHandler<OrderEventArgs>? OrderCancelled;

    // 用受保护的方法封装触发逻辑,方便子类重写
    protected virtual void OnOrderPlaced(OrderEventArgs e)
    {
        // ?. 确保没有订阅者时不会崩
        OrderPlaced?.Invoke(this, e);
    }

    protected virtual void OnOrderCancelled(OrderEventArgs e)
    {
        OrderCancelled?.Invoke(this, e);
    }

    public void PlaceOrder(int orderId, decimal amount)
    {
        Console.WriteLine($"[OrderService] 处理订单 #{orderId},金额:{amount:C}");
        // 业务逻辑处理完毕,开口通知
        OnOrderPlaced(new OrderEventArgs(orderId, amount));
    }

    public void CancelOrder(int orderId, decimal amount)
    {
        Console.WriteLine($"[OrderService] 取消订单 #{orderId}");
        OnOrderCancelled(new OrderEventArgs(orderId, amount));
    }
}

// ── 订阅者:邮件服务 ────────────────────────────
public class EmailService
{
    public void OnOrderPlaced(object? sender, OrderEventArgs e)
    {
        Console.WriteLine($"  [EmailService] 发送确认邮件:订单 #{e.OrderId},金额 {e.Amount:C}");
    }

    public void OnOrderCancelled(object? sender, OrderEventArgs e)
    {
        Console.WriteLine($"  [EmailService] 发送取消通知:订单 #{e.OrderId}");
    }
}

// ── 订阅者:库存服务 ────────────────────────────
public class InventoryService
{
    public void OnOrderPlaced(object? sender, OrderEventArgs e)
    {
        Console.WriteLine($"  [InventoryService] 锁定库存:订单 #{e.OrderId}");
    }

    public void OnOrderCancelled(object? sender, OrderEventArgs e)
    {
        Console.WriteLine($"  [InventoryService] 释放库存:订单 #{e.OrderId}");
    }
}

// ── 主程序 ──────────────────────────────────────
class Program
{
    static void Main()
    {
        var orderService     = new OrderService();
        var emailService     = new EmailService();
        var inventoryService = new InventoryService();

        // 订阅:告诉发布者"我关心这件事"
        orderService.OrderPlaced    += emailService.OnOrderPlaced;
        orderService.OrderPlaced    += inventoryService.OnOrderPlaced;
        orderService.OrderCancelled += emailService.OnOrderCancelled;
        orderService.OrderCancelled += inventoryService.OnOrderCancelled;

        orderService.PlaceOrder(1001, 299.99m);
        Console.WriteLine();
        orderService.CancelOrder(1001, 299.99m);

        Console.WriteLine("\n--- 邮件服务退订后 ---\n");

        // 退订:从此这个事件跟我无关
        orderService.OrderPlaced -= emailService.OnOrderPlaced;

        orderService.PlaceOrder(1002, 599.00m);
    }
}

运行结果清晰地展示了多播的威力------一次 PlaceOrder,两个服务同时响应:

csharp 复制代码
[OrderService] 处理订单 #1001,金额:¥299.99
  [EmailService] 发送确认邮件:订单 #1001,金额 ¥299.99
  [InventoryService] 锁定库存:订单 #1001

[OrderService] 取消订单 #1001
  [EmailService] 发送取消通知:订单 #1001
  [InventoryService] 释放库存:订单 #1001

--- 邮件服务退订后 ---

[OrderService] 处理订单 #1002,金额:¥599.00
  [InventoryService] 锁定库存:订单 #1002

⚡ Lambda 订阅:方便,但有代价

不想专门写一个方法?Lambda 直接订阅也行,代码更紧凑:

csharp 复制代码
orderService.OrderPlaced += (sender, e) =>
{
    Console.WriteLine($"Lambda 收到:订单 #{e.OrderId}");
};

但这里有个坑要记住:Lambda 订阅之后无法退订 。每次写 += 后面跟 Lambda,编译器都会生成一个新的匿名对象,你手里根本没有引用,自然也就没法 -=。需要退订的场景,老老实实用具名方法。


⚙️ event 与裸委托:一张表说清楚

两者看起来相似,本质上差了一层封装:

特性 裸委托 event 事件
外部直接调用 ✅ 允许 ❌ 禁止
外部直接赋值 = ✅ 允许(危险) ❌ 禁止
多播支持
封装性
推荐场景 回调、函数传递 对象间事件通知

简单说:委托是机制,事件是规范 。在对象通信的场景里,永远优先选 event


🎯 几条用好事件的心得

写事件代码时,有几个习惯养成了会省很多麻烦。触发事件时始终用 ?.Invoke() ,一个问号号就能挡住空引用异常。把触发逻辑封装进 protected virtual OnXxx() 方法,子类想改行为直接重写,干净利落。

内存泄漏是事件最常见的隐患------只要订阅者还挂在发布者的事件上,GC 就不会回收它,哪怕你以为它早该消失了。用完记得 -=,这个习惯在长生命周期对象里尤其关键。

事件驱动的代码写顺了,你会发现对象之间的耦合悄悄松开了------每个模块只管自己的事,通过事件说话,整个系统反而比紧密绑定时更容易扩展和测试。这大概就是它在 .NET 里活了这么多年、依然是核心特性的原因。

相关推荐
狗头大军之江苏分军3 小时前
《潮水漫过十七岁》开学了
后端
苏三说技术4 小时前
如何看待GPT-6在UP主众测中碾压夺冠?它是现在最强大模型吗?
后端
mldong4 小时前
一份 JSON,一条能跑的审批流:把报销流程送上工作流引擎
后端·架构
wno7044 小时前
Spring Boot WebFlux增删改查
java·spring boot·后端
Captaincc4 小时前
AI用量v0.1.11更新发布 新增 jusage doctor 诊断指令 托盘展示token 和余额 新增 AutoClaw 支持
前端·后端·vibecoding
aramae5 小时前
模拟实现strlen()函数 (C语言)
c语言·开发语言·后端
IT_陈寒6 小时前
Python的GIL把我坑惨了,多线程跑得比单线程还慢
前端·人工智能·后端
分支预测失败7 小时前
RISC-V 时间子系统深度专题:mtime 访问路径、SBI 定时器与 Linux tickless 协同
后端
65岁退休Coder7 小时前
PI Agent 开发一个生产级 Harness
后端·node.js·agent
用户8356290780517 小时前
如何使用 Python 给 Word 文档添加水印
后端·python