用 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
手动管理观察者列表适合需要精细控制的场景
当标准
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,就无法干净取消——所以生产代码中,事件处理必须是命名方法。
