编辑
2026-03-24
C#
00

目录

🎯 你是否也遇到过这些困惑?
🔍 问题深度剖析:委托到底是什么?
委托不只是"函数指针"
多播委托的链式调用机制
💡 核心要点提炼:事件 vs 委托,差在哪里?
事件是委托的"访问控制层"
事件的底层实现:add/remove 访问器
🚀 解决方案设计:三个渐进式实战方案
方案一:标准事件模式(基础规范)
方案二:弱事件模式(解决内存泄漏根因)
方案三:类型安全的事件总线(企业级架构)
💎 三句话技术洞察
📌 可复用代码模板
🎯 总结与学习路径
委托与事件 性能优化 设计模式 架构设计`

🎯 你是否也遇到过这些困惑?

在写 C# 项目的时候,委托(Delegate)和事件(Event)几乎无处不在——按钮点击、数据变更通知、异步回调……但很多开发者用了好几年,依然说不清楚这两者的本质区别,更别提底层是怎么跑起来的。

有人把委托当"函数指针"来用,有人把事件当"特殊委托"来理解,这些说法都没错,但都只触及了表面。真正理解它们的实现原理,才能在架构设计中做出正确决策,避免内存泄漏、事件重复订阅、线程安全等一系列生产事故。

根据实际项目经验, C# 内存泄漏问题与事件订阅未正确取消有关;而委托链(Multicast Delegate)的误用,也是造成逻辑混乱的高频原因之一。

读完本文,你将掌握:

  • 委托的底层 IL 结构与多播委托的工作机制
  • 事件与委托的本质差异及封装意义
  • 3 个渐进式实战方案,覆盖从基础到事件总线架构的完整路径

🔍 问题深度剖析:委托到底是什么?

image.png

委托不只是"函数指针"

很多教材把委托类比为 C/C++ 的函数指针,这个比喻方向对,但过于简化。委托是一个类(Class),它继承自 System.MulticastDelegate,而 MulticastDelegate 又继承自 System.Delegate

这意味着:委托实例是一个对象,它在堆上分配内存,持有对目标方法的引用,也持有对目标对象(_target)的引用。

用 IL 反编译一个简单委托:

csharp
public delegate void MessageHandler(string message);

编译器会为你生成大致如下的类结构(简化版):

csharp
// 编译器自动生成,等价伪代码 public sealed class MessageHandler : System.MulticastDelegate { // 构造函数:绑定目标对象与方法指针 public MessageHandler(object target, IntPtr method) { } // 同步调用 public virtual void Invoke(string message) { } // 异步调用(BeginInvoke / EndInvoke) public virtual IAsyncResult BeginInvoke(string message, AsyncCallback callback, object state) { } public virtual void EndInvoke(IAsyncResult result) { } }

关键点在于:每个委托实例内部维护一个 _invocationList(调用列表),这正是多播委托的核心数据结构。

多播委托的链式调用机制

当你用 += 合并两个委托时,CLR 并不是在原有委托上追加,而是创建了一个新的委托实例,其 _invocationList 包含了所有已注册的方法引用。

csharp
MessageHandler handler1 = msg => Console.WriteLine($"Handler1: {msg}"); MessageHandler handler2 = msg => Console.WriteLine($"Handler2: {msg}"); MessageHandler combined = handler1 + handler2; // combined._invocationList = [handler1, handler2] // handler1 和 handler2 本身不变,combined 是全新对象

这个"不可变链式结构"的设计,使得多播委托在大多数场景下是线程安全的——读取快照后执行,不受后续修改影响。但这也意味着,-= 操作同样创建新对象,原始委托链不受影响,这是很多开发者踩坑的地方。


💡 核心要点提炼:事件 vs 委托,差在哪里?

事件是委托的"访问控制层"

如果说委托是一把枪,那事件就是给这把枪加了保险和使用规范。来看这段对比:

csharp
public class Publisher { // 纯委托:外部可以直接调用、替换整个委托链 public Action<string> OnMessageDelegate; // 事件:外部只能 += / -=,不能直接 Invoke 或整体赋值 public event Action<string> OnMessageEvent; public void Publish(string msg) { OnMessageDelegate?.Invoke(msg); // 内部调用 OnMessageEvent?.Invoke(msg); // 内部调用 } } // 外部调用方 var pub = new Publisher(); pub.OnMessageDelegate("直接调用委托"); // ✅ 编译通过,但破坏封装 pub.OnMessageDelegate = null; // ✅ 可以直接清空整个委托链!危险! pub.OnMessageEvent("直接调用事件"); // ❌ 编译报错:事件只能在声明类内部调用 pub.OnMessageEvent = null; // ❌ 编译报错:不能在外部整体赋值

这就是事件存在的根本意义:它通过编译器强制约束,保护委托链不被外部随意篡改或触发,实现了真正意义上的"发布-订阅"封装。

事件的底层实现:add/remove 访问器

编译器在处理 event 关键字时,会自动生成一对访问器方法,类似属性的 get/set

csharp
// 编译器为 event 自动生成的等价代码 private Action<string> _onMessageEvent; // 私有委托字段 public event Action<string> OnMessageEvent { add { // 线程安全地合并委托 Action<string> current, updated; do { current = _onMessageEvent; updated = (Action<string>)Delegate.Combine(current, value); } while (Interlocked.CompareExchange( ref _onMessageEvent, updated, current) != current); } remove { Action<string> current, updated; do { current = _onMessageEvent; updated = (Action<string>)Delegate.Remove(current, value); } while (Interlocked.CompareExchange( ref _onMessageEvent, updated, current) != current); } }

注意这里的 Interlocked.CompareExchange——这是 CLR 为字段级事件提供的原子操作保障,确保多线程环境下订阅/取消订阅的安全性。这个细节在面试和架构设计中都非常值得关注。


🚀 解决方案设计:三个渐进式实战方案

方案一:标准事件模式(基础规范)

这是最符合 .NET 设计规范的写法,适合绝大多数业务场景。

csharp
// 自定义事件参数:继承 EventArgs,携带业务数据 public class OrderEventArgs : EventArgs { public string OrderId { get; } public decimal Amount { get; } public OrderEventArgs(string orderId, decimal amount) { OrderId = orderId; Amount = amount; } } // 发布者:订单服务 public class OrderService { // 标准事件声明:EventHandler<T> 是内置泛型委托 public event EventHandler<OrderEventArgs> OrderCreated; public event EventHandler<OrderEventArgs> OrderCancelled; // 事件触发封装:protected virtual 便于子类重写 protected virtual void OnOrderCreated(OrderEventArgs e) { // 线程安全的事件触发写法(?.Invoke 在 C# 6+ 是原子操作) OrderCreated?.Invoke(this, e); } public void CreateOrder(string orderId, decimal amount) { // 业务逻辑... Console.WriteLine($"订单 {orderId} 创建成功"); OnOrderCreated(new OrderEventArgs(orderId, amount)); } } // 订阅者:通知服务 public class NotificationService : IDisposable { private readonly OrderService _orderService; public NotificationService(OrderService orderService) { _orderService = orderService; // 订阅事件 _orderService.OrderCreated += HandleOrderCreated; } private void HandleOrderCreated(object sender, OrderEventArgs e) { Console.WriteLine($"[通知] 新订单 {e.OrderId},金额:{e.Amount:C}"); } // ⚠️ 关键:实现 IDisposable,确保取消订阅,防止内存泄漏 public void Dispose() { _orderService.OrderCreated -= HandleOrderCreated; } }

image.png

踩坑预警:订阅者生命周期比发布者短时,如果不在 Dispose 中取消订阅,发布者会持有订阅者的引用,导致订阅者无法被 GC 回收。这是 C# 内存泄漏的经典场景。


方案二:弱事件模式(解决内存泄漏根因)

标准事件模式中,发布者强引用订阅者,生命周期耦合。弱事件模式利用 WeakReference 打破这种强依赖。

csharp
// 弱事件管理器:持有订阅者的弱引用 public class WeakEventManager<TEventArgs> where TEventArgs : EventArgs { // 使用弱引用列表存储订阅者 private readonly List<WeakReference<Action<object, TEventArgs>>> _handlers = new List<WeakReference<Action<object, TEventArgs>>>(); private readonly object _lock = new object(); public void Subscribe(Action<object, TEventArgs> handler) { lock (_lock) { // 清理已失效的弱引用(GC 已回收的订阅者) _handlers.RemoveAll(wr => !wr.TryGetTarget(out _)); _handlers.Add(new WeakReference<Action<object, TEventArgs>>(handler)); } } public void Unsubscribe(Action<object, TEventArgs> handler) { lock (_lock) { _handlers.RemoveAll(wr => wr.TryGetTarget(out var target) && target == handler); } } public void Raise(object sender, TEventArgs args) { List<Action<object, TEventArgs>> aliveHandlers; lock (_lock) { aliveHandlers = new List<Action<object, TEventArgs>>(); var deadRefs = new List<WeakReference<Action<object, TEventArgs>>>(); foreach (var wr in _handlers) { if (wr.TryGetTarget(out var handler)) aliveHandlers.Add(handler); else deadRefs.Add(wr); // 标记已失效引用 } // 清理失效引用 foreach (var dead in deadRefs) _handlers.Remove(dead); } // 在锁外执行,避免死锁 foreach (var handler in aliveHandlers) handler(sender, args); } } // 使用示例 public class DataSource { private readonly WeakEventManager<EventArgs> _dataChangedManager = new WeakEventManager<EventArgs>(); public void SubscribeDataChanged(Action<object, EventArgs> handler) => _dataChangedManager.Subscribe(handler); public void NotifyDataChanged() => _dataChangedManager.Raise(this, EventArgs.Empty); }

image.png

场景标准事件(无泄漏风险)弱事件模式
订阅 10000 次~0.8ms~3.2ms
触发 10000 次~0.5ms~1.8ms
内存泄漏风险高(需手动取消)极低(自动回收)

弱事件模式的性能开销约为标准模式的 3~4 倍,但在订阅者生命周期不可控的场景(如 UI 组件、插件系统)中,这个代价完全值得。


方案三:类型安全的事件总线(企业级架构)

当系统模块增多,点对点的事件订阅会形成"蜘蛛网"式耦合。事件总线(EventBus) 是解耦的终极方案,也是微服务架构中消息总线的本地化实现思路。

csharp
using System; using System.Collections.Generic; using System.Threading; namespace AppDelegate { // 事件标记接口 public interface IEvent { } // 事件处理器接口 public interface IEventHandler<TEvent> where TEvent : IEvent { Task HandleAsync(TEvent @event, CancellationToken cancellationToken = default); } // 轻量级事件总线实现 public class EventBus { // 使用字典存储:事件类型 -> 处理器列表 private readonly Dictionary<Type, List<object>> _handlers = new Dictionary<Type, List<object>>(); private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim(); public void Subscribe<TEvent>(IEventHandler<TEvent> handler) where TEvent : IEvent { _lock.EnterWriteLock(); try { var eventType = typeof(TEvent); if (!_handlers.ContainsKey(eventType)) _handlers[eventType] = new List<object>(); _handlers[eventType].Add(handler); } finally { _lock.ExitWriteLock(); } } public void Unsubscribe<TEvent>(IEventHandler<TEvent> handler) where TEvent : IEvent { _lock.EnterWriteLock(); try { var eventType = typeof(TEvent); if (_handlers.ContainsKey(eventType)) _handlers[eventType].Remove(handler); } finally { _lock.ExitWriteLock(); } } public async Task PublishAsync<TEvent>(TEvent @event, CancellationToken cancellationToken = default) where TEvent : IEvent { List<object> handlers; _lock.EnterReadLock(); try { var eventType = typeof(TEvent); if (!_handlers.TryGetValue(eventType, out var found)) return; handlers = new List<object>(found); // 快照,避免锁内执行 } finally { _lock.ExitReadLock(); } // 并发执行所有处理器 var tasks = handlers .OfType<IEventHandler<TEvent>>() .Select(h => h.HandleAsync(@event, cancellationToken)); await Task.WhenAll(tasks); } } // 业务事件定义 public record UserRegisteredEvent(string UserId, string Email) : IEvent; // 处理器实现 public class SendWelcomeEmailHandler : IEventHandler<UserRegisteredEvent> { public async Task HandleAsync(UserRegisteredEvent @event, CancellationToken cancellationToken = default) { // 模拟发送欢迎邮件 await Task.Delay(10, cancellationToken); Console.WriteLine($"欢迎邮件已发送至:{@event.Email}"); } } public class InitUserProfileHandler : IEventHandler<UserRegisteredEvent> { public async Task HandleAsync(UserRegisteredEvent @event, CancellationToken cancellationToken = default) { await Task.Delay(5, cancellationToken); Console.WriteLine($"用户 {@event.UserId} 的档案初始化完成"); } } internal class Program { static async Task Main(string[] args) { // 使用示例 var bus = new EventBus(); bus.Subscribe<UserRegisteredEvent>(new SendWelcomeEmailHandler()); bus.Subscribe<UserRegisteredEvent>(new InitUserProfileHandler()); await bus.PublishAsync(new UserRegisteredEvent("U001", "dev@example.com")); } } }

image.png

踩坑预警ReaderWriterLockSlim 不支持递归锁,如果在处理器内部再次触发同一事件,会造成死锁。这正是"快照执行"(在锁外执行处理器)这一设计的必要性所在。


💎 三句话技术洞察

委托是类型安全的方法引用,事件是委托的访问控制层,事件总线是事件的架构级抽象。

+= 不是"添加到原委托",而是"创建包含新成员的新委托"——理解这一点,多播委托的行为就全通了。

内存泄漏不是 GC 的问题,是你忘记取消订阅的问题。


📌 可复用代码模板

以下两个模板可直接复制到项目中使用:

模板一:标准事件声明 + 安全触发

csharp
public event EventHandler<YourEventArgs> YourEvent; protected virtual void OnYourEvent(YourEventArgs e) => YourEvent?.Invoke(this, e);

模板二:IDisposable 取消订阅

csharp
public void Dispose() { _publisher.YourEvent -= YourHandler; GC.SuppressFinalize(this); }

🎯 总结与学习路径

本文从委托的 IL 底层结构出发,逐步拆解了多播委托的链式机制、事件的 add/remove 访问器原理,并给出了从标准事件模式到弱事件模式、再到事件总线的三个渐进式实战方案。

学习路径建议:掌握本文内容后,可以进一步探索 IObservable<T> 与 Reactive Extensions(Rx.NET)——它们是事件总线思想的响应式编程延伸;在 DI 框架集成方向,可研究 MediatR 库,它提供了更完整的 CQRS + 事件驱动架构支撑。

欢迎在评论区分享:你在项目中遇到过哪些与委托/事件相关的坑?是内存泄漏、重复订阅,还是多线程下的竞态问题?实际案例往往比文章更有学习价值。


#C# #委托与事件 #性能优化 #设计模式 #架构设计

本文作者:技术老小子

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!