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 里活了这么多年、依然是核心特性的原因。

相关推荐
2501_928996221 小时前
数据安全与灾备技术
后端·数据安全
磊 子1 小时前
硬中断 软中断
后端·操作系统
妖孽白YoonA1 小时前
xlt-token v1.0.0 正式发布:NestJS / Express 一包接入的 Token 鉴权库
后端·node.js·nestjs
MariaH1 小时前
Stream读写操作
后端
Oo_行者_oO1 小时前
Spring Authorization Server 下 Token 刷新流程自定义实现
后端·面试
alwaysrun1 小时前
C++之灵活易用的YAML解析库yaml-cpp
c++·后端·程序员
pe7er1 小时前
AI为啥会写出if(obj != null && obj.ifEnabled)这样的代码
前端·后端·架构
狗凯之家源码网2 小时前
电商代付系统从零搭建与实战指南
前端·后端·开源
IT_陈寒2 小时前
Vue组件通信这个坑我跳了两次才知道怎么爬出来
前端·人工智能·后端