公司动态
C#观察者模式:事件驱动编程实战指南
1. 观察者模式编程世界的杂志订阅机制想象一下你订阅了一份科技杂志。每当新刊发布出版社就会自动把杂志送到你家而你无需每天打电话询问是否出刊。观察者模式Observer Pattern正是这种机制在软件世界的完美映射——它让对象之间建立一种订阅/通知的依赖关系当被观察对象状态变化时所有依赖它的对象都会自动收到通知。在C#中实现观察者模式本质上是在构建一个松耦合的消息分发系统。与直接调用方法不同观察者模式通过事件驱动的方式让发布者出版社和订阅者读者彼此独立演化。这种设计尤其适合GUI事件处理、实时数据监控等场景比如WinForm中的按钮点击事件就是典型的观察者模式实现。关键认知观察者模式不是简单的回调函数其核心价值在于支持一对多的依赖关系且观察者的增减不会影响被观察者的核心逻辑。2. 模式结构与C#实现剖析2.1 经典UML结构解构观察者模式包含两个核心角色Subject主题维护观察者列表提供添加/删除观察者的方法状态改变时通知所有观察者Observer观察者定义更新接口供主题通知时调用在C#中这个结构可以通过接口明确约束// 观察者接口 public interface IObserver { void Update(string message); } // 主题接口 public interface ISubject { void RegisterObserver(IObserver observer); void RemoveObserver(IObserver observer); void NotifyObservers(); }2.2 C#特色实现方案C#提供了更优雅的内置支持——事件机制。下面是一个杂志订阅的完整示例// 出版社主题 public class MagazinePublisher { public event Actionstring MagazinePublished; private string _latestIssue; public void PublishNewIssue(string issueName) { _latestIssue issueName; OnMagazinePublished(); } protected virtual void OnMagazinePublished() { MagazinePublished?.Invoke(_latestIssue); } } // 订阅者观察者 public class Subscriber { private string _name; public Subscriber(string name) { _name name; } public void ReceiveMagazine(string issueName) { Console.WriteLine(${_name} 收到新刊: {issueName}); } } // 使用示例 var publisher new MagazinePublisher(); var subscriber1 new Subscriber(张三); var subscriber2 new Subscriber(李四); publisher.MagazinePublished subscriber1.ReceiveMagazine; publisher.MagazinePublished subscriber2.ReceiveMagazine; publisher.PublishNewIssue(《C#设计模式实战》2023年10月刊);输出结果张三 收到新刊: 《C#设计模式实战》2023年10月刊 李四 收到新刊: 《C#设计模式实战》2023年10月刊2.3 实现要点解析事件委托类型选择简单通知使用Action需要传递复杂数据时定义自定义委托遵循.NET规范事件处理方法返回void参数为(object sender, EventArgs e)线程安全考虑// 多线程环境下的安全发布 protected virtual void OnMagazinePublished() { var handlers MagazinePublished; if (handlers ! null) { foreach (Actionstring handler in handlers.GetInvocationList()) { handler.BeginInvoke(_latestIssue, null, null); } } }弱引用优化 当观察者生命周期不确定时可使用WeakReference避免内存泄漏public class WeakObserver { private readonly WeakReferenceIObserver _observer; public WeakObserver(IObserver observer) { _observer new WeakReferenceIObserver(observer); } public bool TryGetObserver(out IObserver observer) { return _observer.TryGetTarget(out observer); } }3. 实战应用场景深度探索3.1 WinForm/WPF事件系统Windows桌面开发中按钮点击事件正是观察者模式的典型应用button1.Click (sender, e) { MessageBox.Show(按钮被点击); };背后的实现机制Button类维护一个Click事件列表当用户点击时调用OnClick方法触发所有注册的处理程序使用EventHandler委托类型保证参数一致性3.2 实时数据监控系统构建一个股价监控系统public class StockTicker : ISubject { private ListIObserver _observers new ListIObserver(); private decimal _price; public decimal Price { get _price; set { if (_price ! value) { _price value; NotifyObservers(); } } } public void RegisterObserver(IObserver observer) _observers.Add(observer); public void RemoveObserver(IObserver observer) _observers.Remove(observer); public void NotifyObservers() { foreach (var observer in _observers) { observer.Update($股价更新: {_price:C}); } } } public class StockDisplay : IObserver { public void Update(string message) { Console.WriteLine($[{DateTime.Now:HH:mm:ss}] {message}); } }3.3 跨模块通信解耦在大型系统中使用观察者模式实现模块间通信// 全局事件中心 public static class EventCenter { public static event Actionstring UserLoggedIn; public static event Actionstring OrderCreated; public static void TriggerUserLogin(string username) { UserLoggedIn?.Invoke(username); } public static void TriggerOrderCreate(string orderId) { OrderCreated?.Invoke(orderId); } } // 不同模块的处理器 public class AuditLogger { public AuditLogger() { EventCenter.UserLoggedIn OnUserLoggedIn; } private void OnUserLoggedIn(string username) { File.AppendAllText(audit.log, ${DateTime.Now}: 用户 {username} 登录\n); } }4. 高级技巧与性能优化4.1 观察者模式的衍生变体推模型 vs 拉模型推模型主题将变更数据直接推送给观察者上文示例拉模型观察者收到通知后主动从主题拉取数据// 拉模型接口 public interface IPullObserver { void Update(ISubject subject); } public interface IPullSubject { object GetState(); }事件聚合器模式 更复杂的中间件实现支持基于主题的过滤public class EventAggregator { private DictionaryType, ListDelegate _handlers new DictionaryType, ListDelegate(); public void SubscribeT(ActionT handler) { if (!_handlers.ContainsKey(typeof(T))) { _handlers[typeof(T)] new ListDelegate(); } _handlers[typeof(T)].Add(handler); } public void PublishT(T message) { if (_handlers.TryGetValue(typeof(T), out var handlers)) { foreach (var handler in handlers) { ((ActionT)handler)(message); } } } }4.2 性能关键点通知顺序控制// 通过优先级控制执行顺序 public class PriorityObserver { public int Priority { get; set; } public IObserver Observer { get; set; } } // 注册时指定优先级 subject.RegisterObserver(new PriorityObserver { Priority 1, Observer new HighPriorityObserver() });批量通知优化 当需要高频更新时可采用节流机制private System.Timers.Timer _batchTimer; private Liststring _pendingUpdates new Liststring(); public void QueueUpdate(string message) { _pendingUpdates.Add(message); _batchTimer?.Stop(); _batchTimer new System.Timers.Timer(500); _batchTimer.Elapsed (s, e) { if (_pendingUpdates.Count 0) { NotifyObservers(string.Join(\n, _pendingUpdates)); _pendingUpdates.Clear(); } }; _batchTimer.Start(); }异步通知模式public async Task NotifyObserversAsync() { var tasks _observers.Select(o Task.Run(() o.Update(_state)) ); await Task.WhenAll(tasks); }5. 常见陷阱与最佳实践5.1 典型问题排查指南问题现象可能原因解决方案观察者未收到通知1. 未正确注册事件处理器2. 事件触发条件未满足1. 检查操作是否执行2. 添加调试日志确认事件触发内存泄漏观察者未及时注销长期存在的主题1. 实现IDisposable接口2. 使用WeakReference通知顺序混乱观察者注册顺序不可控1. 引入优先级机制2. 避免观察者间依赖性能下降观察者处理耗时阻塞主线程1. 改为异步通知2. 使用队列缓冲5.2 黄金实践准则命名规范事件命名使用动词过去式如UserLoggedIn事件处理器命名以On开头如OnButtonClick线程安全四要素事件注册/注销使用线程安全集合事件触发前复制委托列表考虑使用SynchronizationContext异步处理中捕获异常测试策略[Test] public void Should_NotifyAllObservers_When_StateChanged() { // Arrange var subject new TestSubject(); var observer1 new MockIObserver(); var observer2 new MockIObserver(); // Act subject.RegisterObserver(observer1.Object); subject.RegisterObserver(observer2.Object); subject.State new; // Assert observer1.Verify(o o.Update(new), Times.Once); observer2.Verify(o o.Update(new), Times.Once); }与其它模式协作结合中介者模式管理复杂观察关系使用装饰器模式为观察者添加额外功能通过策略模式动态改变观察者行为6. 现代C#中的演进6.1 IObservable/Observer接口.NET提供了官方实现的观察者模式接口public class NumberGenerator : IObservableint { private ListIObserverint _observers new ListIObserverint(); public IDisposable Subscribe(IObserverint observer) { _observers.Add(observer); return new Unsubscriber(_observers, observer); } public void GenerateNumbers() { for (int i 0; i 10; i) { foreach (var observer in _observers) { observer.OnNext(i); } Thread.Sleep(1000); } foreach (var observer in _observers) { observer.OnCompleted(); } } private class Unsubscriber : IDisposable { private ListIObserverint _observers; private IObserverint _observer; public Unsubscriber(ListIObserverint observers, IObserverint observer) { _observers observers; _observer observer; } public void Dispose() { if (_observer ! null _observers.Contains(_observer)) { _observers.Remove(_observer); } } } }6.2 Reactive Extensions (Rx.NET)响应式编程库提供了更强大的事件处理能力// 创建可观察序列 var observable Observable.Interval(TimeSpan.FromSeconds(1)) .Take(5) .Where(x x % 2 0); // 订阅观察者 var subscription observable.Subscribe( x Console.WriteLine($收到: {x}), ex Console.WriteLine($错误: {ex.Message}), () Console.WriteLine(完成) ); // 取消订阅 subscription.Dispose();6.3 异步数据流 (System.Threading.Channels)对于高性能场景可以使用通道实现观察者模式public class ChannelPublisher { private readonly Channelstring _channel Channel.CreateUnboundedstring(); public ChannelReaderstring Reader _channel.Reader; public async Task PublishAsync(string message) { await _channel.Writer.WriteAsync(message); } } public class ChannelConsumer { public static async Task ConsumeAsync(ChannelReaderstring reader) { await foreach (var message in reader.ReadAllAsync()) { Console.WriteLine($处理消息: {message}); } } }在实际项目中观察者模式最常见的痛点来自生命周期管理。我曾在一个WPF项目中遇到内存泄漏原因是ViewModel订阅了Model的事件但未及时取消订阅。解决方案是实现IDisposable并在Dispose方法中取消事件注册public class MyViewModel : IDisposable { private readonly IDataModel _model; public MyViewModel(IDataModel model) { _model model; _model.DataChanged OnDataChanged; } private void OnDataChanged(object sender, EventArgs e) { // 处理数据变更 } public void Dispose() { _model.DataChanged - OnDataChanged; } }另一个实用技巧是使用ObservableCollectionT的CollectionChanged事件实现UI自动更新。当集合变更时WPF会自动收到通知并更新绑定控件var items new ObservableCollectionstring(); items.CollectionChanged (sender, e) { Console.WriteLine($集合变更动作: {e.Action}); if (e.NewItems ! null) { foreach (var item in e.NewItems) { Console.WriteLine($新增项: {item}); } } }; items.Add(第一项); items.Add(第二项); items.RemoveAt(0);