1. 介紹
在寫程式時,你會不可避免碰到某個部分需要「通知」其他部分某件重要事情發生了的情況。經典例子 — 使用者點擊滑鼠,這個事件需要被處理。在日常生活中我們早就生活在事件的世界:水壺開始吹口哨 — 你聽到信號就趕快去關火。咖啡灑到鍵盤 — 心臟一驟 — 你立刻衝去救筆電。程式設計也遵循相同的規則。
事件 — 是一個機制,允許來源物件(發布者)去通知其他物件(訂閱者)某些變更或動作已發生。可以把它理解為「我發出通知 — 有聽到的人就回應」。
在 C# 裡,事件是基於 delegate 的特殊語法結構。delegate 定義了回調的簽名 — 訂閱者會以什麼樣的方式被呼叫。事件的宣告使用關鍵字 event,delegate 使用關鍵字 delegate。
為什麼需要事件?
- 鬆耦合:發布者不需要知道訂閱者的存在 — 只發出信號。
- 架構彈性:可以在不改變發布者代碼的情況下動態增加或移除處理器。
- 可擴展性:加入新的訂閱者後,馬上就能收到通知。
經典的「發布者-訂閱者」範式
想像有一個類別「火警警報器」(發布者)和一個類別「建築裡的人」(訂閱者)。當警報器觸發時,它會同時向所有人發出信號 — 不管有多少人或他們在哪裡。這就是發布者-訂閱者(或 Observer)模式。
發布者不知道有多少或是哪一些訂閱者 — 它只是發出通知,其他人自行決定要訂閱還是忽略。
在 C# 中它如何工作?
- 發布者:定義事件(event),提供訂閱/退訂機制。
- 訂閱者:訂閱事件並實作處理器(當事件發生時,處理器會被呼叫)。
2. 事件實作示範:第一個例子
從概念轉到程式碼,我們來描述一個最簡模型。假設我們有一個 console 應用,裡面有個 timer 每秒「滴答」,不同的處理器會對此做出反應(例如在 console 輸出「滴」或統計滴答數)。
步驟 1. 定義 delegate 和 事件
public class SimpleTimer
{
// 宣告事件用的 delegate
public delegate void TickEventHandler(object sender, EventArgs e);
// 基於 delegate 的事件
public event TickEventHandler Tick;
public void Start(int count)
{
for (int i = 0; i < count; i++)
{
System.Threading.Thread.Sleep(1000); // 模擬一次滴答!
OnTick(); // 觸發事件!
}
}
protected virtual void OnTick()
{
// 如果有訂閱者就呼叫事件 (Tick != null)
Tick?.Invoke(this, EventArgs.Empty);
}
}
這段在做什麼?
- 定義了 delegate TickEventHandler,有典型簽名 object sender, EventArgs e。
- 事件 Tick — 作為處理器的訂閱點。
- Start 方法模擬「滴答」,定時呼叫 OnTick。
- 在 OnTick 中以安全方式呼叫事件:Tick?.Invoke(..., EventArgs.Empty)。
步驟 2. 訂閱事件
class Program
{
static void Main()
{
var timer = new SimpleTimer();
// 訂閱 Tick 事件
timer.Tick += Timer_Tick;
timer.Start(3);
// 需要的話可以退訂
timer.Tick -= Timer_Tick;
}
static void Timer_Tick(object sender, EventArgs e)
{
Console.WriteLine("滴!");
}
}
我們建立一個 timer,透過 += 運算子訂閱,之後每次滴答都會呼叫處理器。退訂使用 -=。
3. 有用的細節
為什麼事件比「硬編碼」呼叫更好?
如果 SimpleTimer 在 OnTick 直接把東西寫到 console,這個類別就會跟具體動作緊密耦合。事件讓程式更自由:timer 不知道訂閱者會做什麼——發射火箭、寫日誌到檔案或發送 e-mail 都有可能。
事件和 delegate 的重要區別
- delegate 是指向方法的「指標」,而 事件 則是在 delegate 基礎上的存取限制封裝。
- 訂閱者只能訂閱/退訂;外部不能直接觸發事件 — 只有發布者可以。
- 要宣告事件,在 delegate 類型前加上修飾詞 event — 編譯器會處理正確的存取模型。
C# 事件的簡要工作流程
+------------------+ +------------------------------+
| | | |
| 發布者 | <------> | 訂閱者 |
| (Publisher/Event)| | (Subscriber/Handler) |
| | | |
+------------------+ +------------------------------+
| 1) 宣告事件 | 2) 訂閱該事件
| 3) 觸發事件 | 4) 實作處理器
什麼時候使用事件?
- 需要通知不定數量的聽眾時。
- 不想在來源類別內綁定具體行為時。
- UI、非同步互動、系統通知 — 到處都是事件的身影。
在 C# 中實作事件的主要特性
- 外部不能直接觸發事件:只有發布者的程式碼能呼叫 Invoke。
- 訂閱/退訂:使用 +=/-=;可有多個處理器。
- 事件本質上是一個 delegate 列表:事件發生時,會依照訂閱順序呼叫所有處理器。
- 推薦的 delegate:使用 EventHandler 和 EventHandler<TEventArgs> 以便與 .NET 生態系統相容。
4. 新手常見錯誤
忘記檢查是否有訂閱者:Tick != null。更好的做法是使用安全呼叫:Tick?.Invoke(...)。
訂閱了事件但在處理器不需要時沒有退訂。這可能會導致物件被持有在記憶體中而造成 memory leak。
嘗試從外部類別去「呼叫」事件 — 編譯器不允許。不能像呼叫方法那樣寫 game.GameOver(),如果那是事件而不是方法。
沒有遵守 delegate 的簽名。對於事件,建議使用標準的 EventHandler 或 EventHandler<TEventArgs> — 這樣程式碼能和其它 .NET 函式庫互通。
GO TO FULL VERSION