1. 事件声明和命名风格
事件不是简单的委托。它是应用各部分之间通信的独立实体,声明应该清晰易懂。
使用合适的委托类型
在 99% 的情况下使用标准委托:
- EventHandler — 用于不带数据的事件。
- EventHandler<TEventArgs> — 需要传递参数时使用。
标准化能让代码更易维护并且更好地和 .NET 库集成。如果 EventHandler 能满足需求,就不要自己造委托。
public event EventHandler SomethingHappened; // 没有数据
public event EventHandler<MyEventArgs> DataReceived; // 有额外数据
如果需要特别的自定义 —— 可以自己声明委托,但这种情况少见。
事件命名
在 .NET 中事件通常使用过去式命名:Completed、Clicked、Changed、Received。这强调事件已经发生了。
示例:
public event EventHandler DataLoaded; // 数据已加载
public event EventHandler<MessageEventArgs> MessageReceived; // 收到消息
public event EventHandler Saving; // 保存过程开始
有时会用 Changing 表示“之前”的事件,给外部一个介入的机会。
2. 发布者类的组织:受保护的虚方法 OnEvent
总是添加受保护的虚方法来触发事件:这样形成调用的集中点,便于继承时扩展,并让行为可预测。
public class FileLoader
{
public event EventHandler<FileLoadedEventArgs> FileLoaded;
protected virtual void OnFileLoaded(FileLoadedEventArgs e)
{
FileLoaded?.Invoke(this, e);
}
public void Load(string filename)
{
// ... 加载文件的逻辑 ...
OnFileLoaded(new FileLoadedEventArgs(filename));
}
}
public class FileLoadedEventArgs : EventArgs
{
public string FileName { get; }
public FileLoadedEventArgs(string fileName) => FileName = fileName;
}
让只有 OnFileLoaded 去触发事件 —— 更容易维护和测试。
3. 订阅和取消订阅的规则:生命周期,IDisposable
如果订阅者的生命周期短于发布者,务必在订阅者销毁前取消订阅。通常实现 IDisposable 并在 Dispose() 中取消订阅是个好办法。
public class TemporaryListener : IDisposable
{
private readonly Publisher _publisher;
public TemporaryListener(Publisher publisher)
{
_publisher = publisher;
_publisher.DataReceived += HandleData;
}
private void HandleData(object sender, EventArgs e)
{
// 处理数据
}
public void Dispose()
{
_publisher.DataReceived -= HandleData;
}
}
// 使用 using:
using (var listener = new TemporaryListener(myPublisher))
{
// listener 在这里监听事件
}
// 退出 using 后 - Dispose 被调用,已取消订阅
如果忘记取消订阅,发布者会持有对订阅者委托的引用 —— 会导致内存泄漏和“僵尸对象”。
4. 线程安全地触发事件
在多线程代码中,订阅者可能在触发事件的同时被添加或移除。这会导致竞态和 NullReferenceException。使用线程安全的模式:把委托拷贝到本地变量。
protected virtual void OnSomethingHappened()
{
EventHandler handler = SomethingHappened;
handler?.Invoke(this, EventArgs.Empty);
}
在 C# 6+ 可以直接写:
SomethingHappened?.Invoke(this, EventArgs.Empty);
5. 用 EventArgs 而不是 object
不要通过 object 或类字段来传数据。通过继承 EventArgs 做强类型传递。
public class DownloadCompletedEventArgs : EventArgs
{
public string FileName { get; }
public long Size { get; }
public DownloadCompletedEventArgs(string fileName, long size)
{
FileName = fileName;
Size = size;
}
}
public event EventHandler<DownloadCompletedEventArgs> DownloadCompleted;
6. 对事件和订阅者做文档化
写清楚:什么时候触发事件,EventArgs 字段的含义,是否需要取消订阅以及何时取消。
/// <summary>
/// 事件在成功加载数据后发生。
/// </summary>
public event EventHandler<DataLoadedEventArgs> DataLoaded;
7. 事件架构的总结建议
职责分离
发布者只负责通知事实。订阅者自己决定何时订阅和取消订阅。
避免事件“轰炸”
不要在没有必要的情况下每秒触发同一个事件几十次 —— 会造成不必要的开销。
尽量不要用事件做双向通信
事件适合“一对多”通知。双向通信时考虑使用接口、回调或其他机制。
不要在类中保存订阅者的引用
不要显式持有订阅者引用 —— 事件和委托会自动管理订阅关系。
8. 经典反模式
无类型事件
public event Action<object> SomethingHappened; // 不清楚里面是什么
坏处:类型信息丢失,需要强转,可维护性下降。
忘记取消订阅
public class ShortLivedListener
{
public ShortLivedListener(Publisher p) =>
p.DataReceived += DoWork;
private void DoWork(object sender, EventArgs e) { /* ... */ }
// 没有 Dispose,没有取消订阅 => 僵尸对象!
}
违反 SRP
类同时充当发布者、订阅者和处理器 —— 角色混杂。请分离职责。
9. 面试和项目中的实践应用
在很多使用发布-订阅的项目中,良好的事件组织是可扩展性和可维护性的关键。在面试中常被要求:
- 实现一个具有正确类型的事件系统,
- 演示如何管理订阅者的生命周期,
- 解释如何线程安全地触发事件。
干净、文档化、组织良好的事件代码会让你在候选人中脱颖而出。
GO TO FULL VERSION