1. 事件宣告與命名風格
事件不只是 delegate。它是應用不同部份之間溝通的獨立實體,宣告應該要清楚易懂。
使用正確的 delegate 類型
在 99% 的情況下使用標準 delegate:
- EventHandler — 用於無資料的事件。
- EventHandler<TEventArgs> — 當需要傳遞參數時。
標準化讓程式維護和與 .NET 函式庫整合更簡單。如果 EventHandler 適合,就不要自己發明 delegate。
public event EventHandler SomethingHappened; // 沒有資料
public event EventHandler<MyEventArgs> DataReceived; // 有額外資料
如果需要特殊自訂化才宣告自己的 delegate,但這很少見。
事件命名
在 .NET 裡事件通常用過去式命名:Completed、Clicked、Changed、Received。這強調事件已經發生。
範例:
public event EventHandler DataLoaded; // 資料已載入
public event EventHandler<MessageEventArgs> MessageReceived; // 訊息已收到
public event EventHandler Saving; // 開始儲存程序
有時會用 Changing 形式表示「變更前」,讓外部有機會干預。
2. 出版者類別的組織:受保護的 virtual 方法 OnEvent
務必加入受保護的 virtual 方法來觸發事件:這提供集中呼叫點、繼承時易於擴充,以及預期的行為。
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。使用執行緒安全的範式:把 delegate 複製到區域變數。
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. 事件架構的總結建議
分離職責
出版者只負責通報事實。訂閱者自己決定何時訂閱與退訂。
避免事件炸彈
不要沒必要地每秒產生同一個事件好幾十次 — 這會造成過多負擔。
盡量不要用事件做雙向通訊
事件適合「一方通知,多方監聽」的模式。若需要雙向溝通,考慮使用 interface、callback 或其他機制。
不要在類別中直接保留訂閱者
不要顯式保留對訂閱者的參考 — 事件和 delegate 會自動處理這件事。
8. 經典反模式
無型別事件
public event Action<object> SomethingHappened; // 裡面內容不清楚
壞處:型別安全被破壞,需要 cast,維護性下降。
忘記退訂
public class ShortLivedListener
{
public ShortLivedListener(Publisher p) =>
p.DataReceived += DoWork;
private void DoWork(object sender, EventArgs e) { /* ... */ }
// 沒有 Dispose,沒有退訂 => 殭屍物件!
}
違反 SRP
類別同時當出版者、訂閱者和處理器 — 角色混雜。要分離責任。
9. 在面試與專案中的實際應用
在很多採用發布-訂閱的專案中,事件的良好組織是可擴充性與可維護性的關鍵。在面試常被要求:
- 實作一個具正確型別的事件系統,
- 展示如何管理訂閱者生命週期,
- 解釋事件的執行緒安全呼叫。
乾淨、有文件、組織良好的事件程式碼會讓你在候選人中脫穎而出。
GO TO FULL VERSION