news 2026/7/22 5:32:51

C#委托与事件:从核心原理到实战解耦架构设计

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C#委托与事件:从核心原理到实战解耦架构设计

1. 项目概述:为什么我们需要“解耦”?

在C#开发中,尤其是构建具有一定复杂度的桌面应用、游戏逻辑或服务端模块时,我们常常会面临一个经典困境:一个模块的变动,像多米诺骨牌一样,引发一连串其他模块的修改。比如,一个“用户登录成功”的动作,可能需要触发更新界面、记录日志、发送通知、初始化用户数据等多项任务。如果这些任务都通过直接调用其他类的方法来实现,代码就会变得高度耦合,牵一发而动全身,维护和扩展将成为噩梦。

“委托与事件”正是C#为解决这类问题而生的利器,它们构成了观察者模式在C#中的优雅实现。简单来说,委托(Delegate)是一种类型安全的函数指针,它定义了方法的签名;而事件(Event)是基于委托的、遵循特定规范的发布-订阅机制。通过它们,我们可以让一个对象(发布者)在特定事情发生时,通知所有关心此事的其他对象(订阅者),而发布者完全不需要知道订阅者是谁、有多少个、具体做了什么。这就是“解耦”的核心——将变化的可能性隔离在订阅者内部,发布者的代码因此变得稳定而清晰。

掌握委托与事件的解耦技巧,是C#程序员从“能写功能”迈向“会设计结构”的关键一步。这不仅仅是语法知识,更是一种重要的架构思维。无论是WinForms/WPF中的按钮点击、ASP.NET Core中的中间件管道,还是Unity游戏引擎中的消息系统,其底层通信机制都深深植根于此。接下来,我将结合十多年的踩坑经验,为你彻底拆解这套机制,让你不仅能写出解耦的代码,更能理解其背后的设计哲学与实战要点。

2. 核心概念深度解析:委托、事件与解耦的三角关系

要玩转解耦,必须吃透委托和事件各自扮演的角色以及它们如何协同工作。很多人学了语法,但用起来还是别扭,问题往往出在对三者关系的理解不够透彻。

2.1 委托:能力契约的缔造者

你可以把委托想象成一份“能力说明书”或“合同”。它不关心是谁来履行合同,只关心履行者必须具备什么样的“能力”(即方法的参数和返回值类型)。

// 定义一个委托类型,它描述了一种“能力”:接收一个string参数,返回void。 public delegate void LogHandler(string message);

这行代码声明了一个名为LogHandler的委托类型。它相当于在说:“任何想为我记录日志的对象,你必须提供一个方法,这个方法能接收一个字符串消息并且不返回任何内容。” 委托是一种类型,和classinterface一样,可以定义在命名空间或类内部。

为什么需要委托类型?因为它提供了类型安全。相比于C/C++中的函数指针,委托是面向对象且类型安全的。编译器会确保你赋值给委托变量的方法,其签名(参数类型、个数、返回值)必须与委托定义完全匹配。

public class ConsoleLogger { // 这个方法符合LogHandler合同 public void WriteToConsole(string msg) { Console.WriteLine($"[Console] {msg}"); } } public class FileLogger { // 这个方法也符合LogHandler合同 public void WriteToFile(string msg) { File.AppendAllText("log.txt", $"{DateTime.Now}: {msg}\n"); } } // 使用委托变量 LogHandler logger; logger = new ConsoleLogger().WriteToConsole; // 赋值方法 logger += new FileLogger().WriteToFile; // 多播委托,追加方法 logger("应用程序启动"); // 一次调用,两个方法都会执行

关键理解点:委托变量(如logger)可以存储对一个或多个方法的引用。使用+=操作符可以组合多个方法(多播委托),调用该委托变量时会按顺序调用所有方法。=是赋值,会清空之前的引用列表;+=是追加;-=是移除。

注意:对于实例方法,委托不仅保存了方法引用,还隐式保存了方法所属对象实例的引用。这就是为什么你可以把new ConsoleLogger().WriteToConsole这样的实例方法赋给委托。

2.2 事件:基于委托的安全发布-订阅机制

如果委托是“能力合同”,那么事件就是一份“标准的招标公告流程”。它基于委托,但增加了严格的访问控制,确保了发布-订阅模式的安全性和封装性。

事件在类内部声明,使用event关键字:

public class OrderService { // 1. 定义委托类型(如果使用系统预定义的,如EventHandler,可省略此步) public delegate void OrderProcessedEventHandler(object sender, OrderEventArgs e); // 2. 基于委托类型声明事件 public event OrderProcessedEventHandler OrderProcessed; }

事件与委托字段的本质区别: 从外部看,事件就像一个被阉割了的委托变量。对于上面的OrderProcessed事件:

  • 订阅者视角:只能做两件事:+=(订阅)和-=(取消订阅)。不能使用=直接赋值,也不能直接Invoke(调用)事件。
    OrderService service = new OrderService(); service.OrderProcessed += OnOrderProcessed; // 正确:订阅 service.OrderProcessed -= OnOrderProcessed; // 正确:取消订阅 // service.OrderProcessed = OnOrderProcessed; // 编译错误:不能直接赋值 // service.OrderProcessed(null, null); // 编译错误:不能直接调用
  • 发布者(OrderService类内部)视角:可以像调用委托一样,检查是否为null后安全地触发事件。
    public void ProcessOrder(Order order) { // ... 处理订单逻辑 ... OnOrderProcessed(new OrderEventArgs { Order = order }); } protected virtual void OnOrderProcessed(OrderEventArgs e) { // 线程安全的触发方式 OrderProcessedEventHandler handler = OrderProcessed; if (handler != null) { handler(this, e); } }

为什么要有事件?直接用公共委托字段不行吗?不行,这关乎封装性安全性。如果OrderProcessed只是一个公共的OrderProcessedEventHandler委托字段,那么任何外部代码都可以:

  1. 使用=操作符,清空所有其他订阅者的列表。
  2. 直接Invoke触发事件,冒充发布者。 这完全破坏了发布-订阅模式的设计初衷。事件通过编译器施加的限制,完美地防止了这些情况,确保了只有事件的拥有者(声明它的类)才能触发它,订阅者只能选择加入或退出。

2.3 解耦:委托与事件如何实现

现在我们把三者串联起来看“解耦”是如何发生的:

  1. 定义契约(委托)OrderProcessedEventHandler定义了“当订单处理完成后要通知谁”的契约。
  2. 提供通知通道(事件)OrderProcessed事件是这个通知通道的入口和出口。
  3. 发布者(OrderService):只负责在正确的业务逻辑点(ProcessOrder方法内)触发OrderProcessed事件。它不关心谁在听,也不关心听众做了什么。它的代码因此变得极其稳定,未来无论是要增加邮件通知、短信通知还是更新排行榜,都无需修改OrderService的代码。
  4. 订阅者(如EmailService, AnalyticsService):它们根据自己的职责,订阅感兴趣的事件(OrderProcessed)。当事件发生时,它们执行自己的逻辑(发邮件、记录分析数据)。订阅者之间也互不知晓,独立变化。

解耦的威力:假设现在需要新增一个“库存扣减服务”在订单处理后更新库存。我们只需要新建一个InventoryService类,让它订阅OrderProcessed事件即可。OrderService和已有的EmailService等模块都无需做任何修改。系统的可扩展性、可维护性得到了质的提升。

3. 标准事件模式与最佳实践

.NET框架为我们设计了一套成熟的事件模式,遵循它能让你的代码更标准、更易被其他开发者理解。

3.1 .NET事件模式标准写法

这套模式的核心是使用System.EventHandler<TEventArgs>泛型委托和自定义的事件参数类。

// 1. 定义事件参数类,继承自EventArgs public class OrderEventArgs : EventArgs { public Order Order { get; set; } public DateTime ProcessedTime { get; set; } } // 2. 发布者类 public class OrderService { // 使用泛型EventHandler<TEventArgs>声明事件 public event EventHandler<OrderEventArgs> OrderProcessed; // 3. 定义触发事件的受保护虚方法(命名通常为OnXXX)。这是最佳实践。 protected virtual void OnOrderProcessed(Order order) { // 线程安全地获取事件委托实例的副本 EventHandler<OrderEventArgs> handler = OrderProcessed; if (handler != null) { handler(this, new OrderEventArgs { Order = order, ProcessedTime = DateTime.Now }); } } public void ProcessOrder(Order order) { // ... 核心业务逻辑 ... // 业务逻辑完成后,触发事件 OnOrderProcessed(order); } } // 4. 订阅者类 public class EmailService { public EmailService(OrderService orderService) { // 订阅事件 orderService.OrderProcessed += OnOrderProcessed; } // 事件处理方法,签名必须与EventHandler<OrderEventArgs>匹配 private void OnOrderProcessed(object sender, OrderEventArgs e) { var order = e.Order; Console.WriteLine($"发送邮件给 {order.CustomerEmail}: 您的订单 {order.Id} 已处理完成。"); } }

为什么这是最佳实践?

  • 标准化EventHandler<T>是.NET BCL中的标准委托,所有.NET开发者都熟悉。
  • sender参数:传递触发事件的对象(this),方便订阅者知道事件来源。
  • 继承EventArgs:方便未来扩展,可以在不改变委托签名的情况下,通过TEventArgs传递更多数据。
  • 受保护的虚方法OnXXX:这是一个重要的设计。它让派生类可以重写事件触发行为(虽然很少需要),更重要的是,它把事件触发的逻辑封装在一个方法里,使ProcessOrder业务方法更清晰。同时,handler局部变量的赋值操作是线程安全的(在赋值那一刻的瞬间快照),避免了在检查null和调用之间,另一个线程取消订阅导致NullReferenceException的风险。

3.2 自定义委托 vs EventHandler

什么时候需要自定义委托?通常不需要。EventHandler<T>已经覆盖了99%的场景。仅在以下情况考虑自定义委托:

  1. 需要不同的方法签名(例如,需要一个返回值)。
  2. 为了极致的性能,避免EventArgs对象的装箱(在性能敏感的循环中,但这种情况极少)。

但请注意,需要返回值的事件模式非常罕见,通常意味着设计可能有问题(事件是通知,而非请求)。绝大多数情况下,坚持使用EventHandler<T>

3.3 事件订阅与内存泄漏陷阱

这是委托和事件使用中最经典的“坑”。由于委托持有对目标对象(订阅者)的引用,如果发布者对象的生命周期长于订阅者,且订阅者没有取消订阅,那么订阅者对象将无法被垃圾回收器(GC)回收,导致内存泄漏。

问题场景

public class Publisher { public event EventHandler SomethingHappened; } public class Subscriber { public Subscriber(Publisher pub) { pub.SomethingHappened += HandleEvent; // 订阅 } private void HandleEvent(object sender, EventArgs e) { } } // 使用 var publisher = new Publisher(); // 长生命周期对象,如静态单例、主窗体 var subscriber = new Subscriber(publisher); subscriber = null; // 试图丢弃subscriber // 此时,由于publisher.SomethingHappened还持有对subscriber.HandleEvent的引用, // subscriber对象无法被GC回收!

解决方案

  1. 显式取消订阅:当订阅者不再需要事件时,或订阅者生命周期结束时,务必使用-=
    public class Subscriber : IDisposable { private Publisher _publisher; public Subscriber(Publisher pub) { _publisher = pub; pub.SomethingHappened += HandleEvent; } public void Dispose() { // 在Dispose中取消订阅 _publisher.SomethingHappened -= HandleEvent; } private void HandleEvent(object sender, EventArgs e) { } }
  2. 使用弱事件模式:对于WPF等框架,存在WeakEventManager或第三方库(如WeakEventHandler),它使用弱引用,允许订阅者被回收。但这会带来轻微的性能开销和更复杂的用法,非必要不使用。
  3. 注意静态事件:静态事件的生命周期与应用域相同,订阅它的实例方法会导致实例永远无法被释放,风险极高。务必谨慎使用静态事件,并确保清理。

实操心得:在桌面应用开发中,窗体(Form)控件的事件订阅是内存泄漏的重灾区。例如,一个自定义控件订阅了主窗体的某个静态事件,当窗体关闭而控件未取消订阅,控件就不会被释放。养成习惯:在窗体的Dispose方法或FormClosing事件中,集中清理所有非本窗体控件发起的事件订阅。

4. 高级技巧与实战应用模式

掌握了基础,我们来看看如何用委托和事件玩出更多花样,解决更复杂的设计问题。

4.1 泛型委托与Lambda表达式的优雅结合

.NET Framework提供了内置的泛型委托Action<>Func<>,它们可以替代很多自定义委托声明,让代码更简洁,尤其是在配合Lambda表达式时。

  • Action<>:表示一个无返回值的方法。Action无参,Action<T>一个参数,以此类推。
  • Func<>:表示一个有返回值的方法。最后一个泛型参数是返回值类型。Func<TResult>无参有返回,Func<T1, TResult>一个参数有返回。

应用场景:回调与策略模式

public class DataProcessor { // 使用Action<string>作为处理日志的回调 public void ProcessData(string data, Action<string> logCallback) { if (string.IsNullOrEmpty(data)) { logCallback?.Invoke("输入数据为空!"); // 安全调用 return; } // ... 处理数据 ... logCallback?.Invoke($"数据{data}处理完成。"); } } // 调用方可以灵活注入日志行为 var processor = new DataProcessor(); // 方式1:传入Lambda表达式 processor.ProcessData("test", msg => Console.WriteLine($"[Info] {msg}")); // 方式2:传入一个方法 processor.ProcessData("test", LogToFile); // 方式3:传入一个匿名方法 processor.ProcessData("test", delegate(string msg) { Debug.WriteLine(msg); }); private void LogToFile(string message) { /*...*/ }

这里,Action<string>logCallback参数使得DataProcessor类与具体的日志实现解耦。调用者决定日志如何输出,这体现了策略模式的思想。

4.2 事件聚合器:实现更彻底的解耦

在大型应用中,直接的事件订阅可能导致复杂的依赖网(A订阅B,B订阅C……)。事件聚合器(Event Aggregator)是一种中介模式,它作为全局的、唯一的事件总线,所有模块都通过它来发布和订阅事件,模块之间完全不知道彼此的存在。

简易实现示例

public interface IEventAggregator { void Publish<TEvent>(TEvent eventToPublish) where TEvent : class; void Subscribe<TEvent>(Action<TEvent> handler) where TEvent : class; void Unsubscribe<TEvent>(Action<TEvent> handler) where TEvent : class; } public class SimpleEventAggregator : IEventAggregator { private readonly Dictionary<Type, List<object>> _handlers = new(); public void Publish<TEvent>(TEvent eventToPublish) where TEvent : class { var eventType = typeof(TEvent); if (_handlers.ContainsKey(eventType)) { // 注意:这里需要处理线程安全和异常隔离 foreach (var handler in _handlers[eventType].Cast<Action<TEvent>>().ToList()) { handler(eventToPublish); } } } public void Subscribe<TEvent>(Action<TEvent> handler) where TEvent : class { var eventType = typeof(TEvent); if (!_handlers.ContainsKey(eventType)) { _handlers[eventType] = new List<object>(); } _handlers[eventType].Add(handler); } public void Unsubscribe<TEvent>(Action<TEvent> handler) where TEvent : class { // 实现省略... } } // 使用 public class OrderCreatedEvent { public Order Order { get; set; } } var eventAggregator = new SimpleEventAggregator(); // 订阅 eventAggregator.Subscribe<OrderCreatedEvent>(e => Console.WriteLine($"订单 {e.Order.Id} 创建了")); // 发布 eventAggregator.Publish(new OrderCreatedEvent { Order = new Order() });

Prism、MvvmCross等框架都提供了成熟的事件聚合器实现。它的优点是极致解耦,缺点是失去了编译时的类型检查(所有事件都是通过泛型TEvent来区分的),并且调试时事件流可能不那么直观。

4.3 异步事件(C# 5.0+)

传统的同步事件处理会阻塞发布者,直到所有订阅者处理完毕。从C# 5.0开始,我们可以利用async/await实现异步事件处理。

定义异步事件

public delegate Task AsyncEventHandler<TEventArgs>(object sender, TEventArgs e); public class AsyncEventPublisher { public event AsyncEventHandler<EventArgs> AsyncWorkCompleted; protected virtual async Task OnAsyncWorkCompleted() { var handler = AsyncWorkCompleted; if (handler != null) { // 依次异步调用所有订阅者,但这里是顺序执行,一个await完才下一个。 // 如果需要并发执行,需使用Task.WhenAll。 foreach (AsyncEventHandler<EventArgs> singleHandler in handler.GetInvocationList()) { await singleHandler(this, EventArgs.Empty); } } } public async Task DoWorkAsync() { await Task.Delay(1000); // 模拟异步工作 await OnAsyncWorkCompleted(); // 异步触发事件 } } // 订阅者 var publisher = new AsyncEventPublisher(); publisher.AsyncWorkCompleted += async (s, e) => { await Task.Delay(500); Console.WriteLine("异步处理完成1"); };

关键点

  1. 事件处理程序返回Task
  2. 发布者触发事件时使用await
  3. 注意异常处理:一个订阅者的异常会传播到发布者。通常需要单独try-catch每个处理程序的调用。
  4. 考虑并发与顺序:上面的例子是顺序执行。如果订阅者之间无依赖,可以使用Task.WhenAll并发执行以提高效率。

5. 常见问题、调试技巧与性能考量

5.1 常见问题速查表

问题现象可能原因解决方案
事件触发了,但订阅者没反应1. 订阅者订阅事件的时机不对(在事件触发后才订阅)。
2. 订阅者方法签名与事件委托不匹配。
3. 订阅者对象已被垃圾回收(如果是弱引用或错误处理)。
1. 确保在触发前订阅(通常在构造函数或初始化方法中)。
2. 检查参数类型、数量和返回值。
3. 检查对象生命周期,确保持有引用。
抛出NullReferenceException触发事件时没有检查nullif (OrderProcessed != null) OrderProcessed(...)在多线程下不安全,可能在检查后、调用前被其他线程置为null使用局部变量副本:var handler = OrderProcessed; if (handler != null) handler(...);
内存使用持续增长事件订阅导致的内存泄漏。长生命周期对象持有短生命周期对象的引用。1. 实现IDisposable,在Dispose中取消订阅。
2. 使用弱事件模式(如WPF的WeakEventManager)。
3. 审查静态事件的订阅。
事件处理顺序不符合预期多播委托的调用顺序是订阅的先后顺序。如果对顺序有强需求,依赖此特性并不稳健,因为订阅顺序可能因代码改动而变化。不要依赖隐式的调用顺序。如果顺序重要,应在发布者内部维护一个明确的有序处理器列表,或者让事件参数包含优先级字段,由订阅者自行处理。
异步事件中异常被吞掉async事件处理中,如果没有await,异常可能不会立即抛出。或者使用Task.WhenAll时未处理聚合异常。1. 确保异步事件处理程序被await
2. 使用try-catch包裹每个处理程序的调用,或处理Task.WhenAll返回的Task的异常。

5.2 调试技巧:谁订阅了我的事件?

在复杂项目中,有时很难追踪是哪些对象订阅了某个事件。可以使用调试工具或反射来查看。

使用Visual Studio调试器

  1. 在触发事件的代码行设置断点。
  2. 当断点命中时,在“即时窗口”中输入?事件名(例如?this.OrderProcessed)。
  3. 展开返回的委托对象,查看_invocationList字段(对于多播委托),里面包含了每个订阅者方法的详细信息,包括目标对象(_target)和方法名(_methodPtr_method)。

通过反射获取订阅列表(用于诊断代码)

public static List<Delegate> GetEventSubscribers(object obj, string eventName) { var field = obj.GetType().GetField(eventName, BindingFlags.Instance | BindingFlags.NonPublic); if (field == null) return new List<Delegate>(); var delegateValue = field.GetValue(obj) as Delegate; if (delegateValue == null) return new List<Delegate>(); return delegateValue.GetInvocationList().ToList(); }

注意:此方法依赖于事件的底层实现(编译器生成的私有委托字段),字段名通常就是事件名。但这属于实现细节,不同编译器版本可能不同,仅限调试使用。

5.3 性能考量

  • 委托调用开销:委托调用比直接方法调用稍慢,因为多了一次间接寻址。但在绝大多数应用场景中,这种开销微乎其微,不应成为不使用委托的理由。清晰的结构带来的收益远大于此开销。
  • 事件触发频率:对于在紧密循环中每秒触发成千上万次的事件,需要评估性能影响。可以考虑使用标志位控制、批量处理或直接调用等优化手段。
  • 多播委托的遍历GetInvocationList()会返回一个委托数组的副本。在性能关键路径中频繁调用它可能产生压力。如果只是需要调用,直接调用多播委托即可(它会遍历内部列表)。

6. 实战:构建一个简单的消息中心

让我们综合运用以上知识,构建一个用于模块间通信的轻量级消息中心。

// 定义消息基类 public abstract class MessageBase { } // 定义具体消息 public class UserLoggedInMessage : MessageBase { public string UserName { get; set; } } public class OrderShippedMessage : MessageBase { public int OrderId { get; set; } public DateTime ShipDate { get; set; } } // 消息中心(单例) public class MessageCenter { private static readonly Lazy<MessageCenter> _instance = new Lazy<MessageCenter>(() => new MessageCenter()); public static MessageCenter Instance => _instance.Value; private readonly Dictionary<Type, List<object>> _subscribers = new(); private MessageCenter() { } // 订阅消息 public void Subscribe<TMessage>(Action<TMessage> handler) where TMessage : MessageBase { var messageType = typeof(TMessage); if (!_subscribers.ContainsKey(messageType)) { _subscribers[messageType] = new List<object>(); } _subscribers[messageType].Add(handler); } // 取消订阅 public void Unsubscribe<TMessage>(Action<TMessage> handler) where TMessage : MessageBase { var messageType = typeof(TMessage); if (_subscribers.ContainsKey(messageType)) { _subscribers[messageType].Remove(handler); } } // 发布消息 public void Publish<TMessage>(TMessage message) where TMessage : MessageBase { var messageType = typeof(TMessage); if (_subscribers.ContainsKey(messageType)) { // 复制列表以避免在遍历过程中集合被修改 var handlers = _subscribers[messageType].Cast<Action<TMessage>>().ToList(); foreach (var handler in handlers) { try { handler(message); } catch (Exception ex) { // 一个订阅者的异常不应影响其他订阅者 // 在实际项目中,这里应该记录日志 Console.WriteLine($"处理消息 {messageType.Name} 时发生异常: {ex.Message}"); } } } } } // 使用示例 public class UIModule { public UIModule() { MessageCenter.Instance.Subscribe<UserLoggedInMessage>(OnUserLoggedIn); } private void OnUserLoggedIn(UserLoggedInMessage msg) { Console.WriteLine($"UI: 欢迎回来,{msg.UserName}!"); } } public class LoggingModule { public LoggingModule() { MessageCenter.Instance.Subscribe<UserLoggedInMessage>(OnUserLoggedIn); MessageCenter.Instance.Subscribe<OrderShippedMessage>(OnOrderShipped); } private void OnUserLoggedIn(UserLoggedInMessage msg) { Console.WriteLine($"日志: 用户 {msg.UserName} 登录系统。"); } private void OnOrderShipped(OrderShippedMessage msg) { Console.WriteLine($"日志: 订单 {msg.OrderId} 已于 {msg.ShipDate} 发货。"); } } // 模拟应用 class Program { static void Main() { var ui = new UIModule(); var logger = new LoggingModule(); // 用户登录 MessageCenter.Instance.Publish(new UserLoggedInMessage { UserName = "张三" }); // 输出: // UI: 欢迎回来,张三! // 日志: 用户 张三 登录系统。 // 订单发货 MessageCenter.Instance.Publish(new OrderShippedMessage { OrderId = 1001, ShipDate = DateTime.Now }); // 输出: // 日志: 订单 1001 已于 [当前时间] 发货。 } }

这个简单消息中心的优点

  1. 完全解耦UIModuleLoggingModule互不知晓,只与MessageCenter交互。
  2. 类型安全:使用泛型,在编译时确保消息类型和处理程序匹配。
  3. 易于扩展:新增消息类型或订阅者无需修改现有代码。
  4. 集中管理:所有跨模块通信在一个地方可见和管理。

可以改进的方向

  • 线程安全:对_subscribers字典的访问应加锁(lock)或使用并发集合(ConcurrentDictionary)。
  • 依赖注入:将MessageCenter作为服务注入,而不是使用单例,便于测试。
  • 异步支持:提供PublishAsync方法和支持Func<TMessage, Task>的订阅。
  • 弱引用:集成弱引用支持以防止内存泄漏。

委托与事件的解耦艺术,精髓在于让对象各司其职,通过“消息”而非“调用”来协作。从最基础的event关键字到复杂的事件聚合器、消息总线,其核心思想一脉相承。理解并善用这一机制,能让你设计的C#应用程序在应对变化时更加从容,架构也更加清晰健壮。在实际编码中,我个人的习惯是:对于类内部的简单回调,优先用Action/Func;对于跨对象的、标准的状态变更通知,用标准event模式;对于大型应用中的跨模块、跨层通信,则引入消息中心或事件聚合器。多思考“这里的变化点是什么”,你就能更准确地判断该在何时、以何种方式使用这把解耦利器。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/22 5:32:25

冥想第一千九百四十八天

1.周二&#xff0c;今天桐桐又跟着来了&#xff0c;傍晚的时候天气阴了&#xff0c;但是还是很热。今天带溪溪游泳。中午吃多了 2.感谢父母&#xff0c;感谢朋友&#xff0c;感谢家人&#xff0c;感谢不断进步的自己。

作者头像 李华
网站建设 2026/7/22 5:31:58

C++异常类设计:从标准库继承到RAII资源管理

1. 项目概述&#xff1a;为什么我们需要“异常类”&#xff1f;在C的世界里摸爬滚打久了&#xff0c;你肯定遇到过这种情况&#xff1a;一个函数执行到一半&#xff0c;因为某个预料之外的问题&#xff08;比如文件打不开、内存分配失败、数组越界访问&#xff09;而崩溃&#…

作者头像 李华
网站建设 2026/7/22 5:31:11

WinForm开发中的Windows消息机制详解与实战

1. WinForm开发中的Window消息机制基础Windows消息机制是WinForm应用程序与操作系统交互的核心通道。每个窗口&#xff08;包括控件&#xff09;都有一个消息队列&#xff0c;系统将用户输入、系统事件等封装成消息投递到这个队列中。作为C#开发者&#xff0c;理解这些消息的工…

作者头像 李华
网站建设 2026/7/22 5:31:00

C语言指针与函数指针实战:从原理到嵌入式开发应用

1. 为什么C语言指针能"封神"&#xff1f;在嵌入式开发领域摸爬滚打十几年&#xff0c;我见过太多被指针劝退的初学者。但真正掌握指针的开发者&#xff0c;往往能用C语言写出堪比面向对象语言的优雅代码。最近接手的一个工业控制器项目&#xff0c;就用函数指针回调实…

作者头像 李华
网站建设 2026/7/22 5:30:56

DVWA靶场——XSS(Stored) 存储型XSS

XSS存储型攻击&#xff0c;攻击者事先将恶意代码上传或储存到漏洞服务器中&#xff0c;只要受害者浏览包含此恶意代码的页面就会执行恶意代码。这就意味着只要访问了这个页面的访客&#xff0c;都有可能会执行这段恶意脚本&#xff0c;因此储存型XSS的危害会更大。因为存储型XS…

作者头像 李华
网站建设 2026/7/22 5:30:07

Bun vs Node.js:新一代JavaScript运行时性能对比

1. Bun与Node.js的性能之争&#xff1a;新一代JavaScript运行时的崛起最近在开发者社区里&#xff0c;Bun这个名词出现的频率越来越高。作为一个长期使用Node.js的后端开发者&#xff0c;我第一次听说Bun能"碾压"Node.js时也是持怀疑态度的。但经过实际测试和源码分析…

作者头像 李华