C# 观察者模式实现方法 C#如何通过事件实现观察者模式

来源:这里教程网 时间:2026-02-21 17:40:03 作者:

event
声明委托是标准做法

在 C# 中,观察者模式最自然、最符合语言习惯的实现方式就是使用

event
关键字。它本质是对委托的封装,提供访问控制(外部只能 += / -=,不能直接赋值或调用),避免观察者意外干扰发布者内部逻辑。

常见错误是直接暴露公共委托字段,比如:

public Action<string> OnDataChanged;</string>
——这会让订阅者能清空整个委托链(
OnDataChanged = null;
),破坏观察者机制。

正确写法是:

public class DataPublisher
{
    // 声明事件:基于 EventHandler 或自定义委托
    public event EventHandler<string> DataChanged;
<pre class='brush:php;toolbar:false;'>public void Notify(string value)
{
    // 线程安全检查(.NET 6+ 可用 null-forgiving,但推荐显式判空)
    DataChanged?.Invoke(this, value);
}

}

EventHandler<t></t>
比原生
Action
更合适

虽然可以用

public event Action<string> DataChanged;</string>
,但不推荐。原因有三:

EventHandler<t></t>
是 .NET 标准约定,第一个参数固定为
object sender
,便于观察者识别事件来源;
它天然支持 WinForms/WPF/ASP.NET 等框架的事件系统,后续扩展更平滑; 当需要取消订阅或区分多个发布者时,
sender
是唯一可靠依据,而
Action
没有该信息。

若需传递多个参数,不要拼接字符串或用

object[]
,应定义专用事件参数类:

public class DataChangedEventArgs : EventArgs
{
    public string Value { get; }
    public DateTime Timestamp { get; }
    public bool IsCritical { get; }
<pre class='brush:php;toolbar:false;'>public DataChangedEventArgs(string value, bool isCritical = false)
{
    Value = value;
    Timestamp = DateTime.UtcNow;
    IsCritical = isCritical;
}

}

// 使用方式 public event EventHandler DataChanged;

手动管理观察者列表适合需要精细控制的场景

当标准

event
不够用时——比如要支持优先级订阅、条件过滤、运行时暂停通知、或需遍历所有观察者做状态检查——就得绕过
event
,自己维护
List<action>></action>
Dictionary<string action>></string>

这时要注意:

必须加锁(如
lock(_lock)
)或用线程安全集合(
ConcurrentBag<action>></action>
),否则多线程订阅/取消会引发
InvalidOperationException
取消订阅不能只靠
list.Remove(handler)
,因为委托相等性判断复杂,建议用唯一 ID 或
WeakReference
避免内存泄漏;
Invoke
时若某个观察者抛异常,默认会中断后续调用,需用
try/catch
包裹单个处理逻辑。

别忽略事件生命周期与内存泄漏

最常见的坑是:观察者(尤其是 UI 控件或长期存活对象)订阅了事件,但没在销毁时取消订阅,导致发布者无法被 GC 回收。

典型表现:

窗体关闭后仍收到通知; 性能分析器显示某类实例数持续增长; 调试时发现
publisher.DataChanged
的委托链里还挂着已释放对象的方法。

解决方法很简单,但必须成对出现:

// 订阅
publisher.DataChanged += OnPublisherDataChanged;
<p>// 取消(例如在 Form.Closing、IDisposable.Dispose、或 ViewModel 的 Cleanup 中)
publisher.DataChanged -= OnPublisherDataChanged;

如果使用匿名函数或 lambda,就无法干净取消——所以生产代码中,事件处理必须是命名方法。

相关推荐