1. 介紹
所以,當我們在 C# 的語境下說「事件」(event)時,意思是讓一個或多個物件可以被安全地通知某事已發生的機制。事件並不是魔術:底層是 delegate(關鍵字 delegate),並加上防止被誤用的保護。
想像訂閱 YouTube 頻道。頻道(事件擁有者物件)有個「訂閱」按鈕——任何觀眾(其他物件)都可以按它加入訂閱者清單(delegate 處理器)。當作者上傳新影片(觸發事件)時,通知只會發給訂閱者,且只有頻道作者決定何時觸發。重要的一點:觀眾可以訂閱或取消訂閱,但不能自己發送通知——只有頻道擁有者能這樣做。
一點形式化的語法
// 聲明一個 delegate,定義事件處理器的「形狀」
public delegate void SimpleEventHandler();
// 基於 delegate 聲明事件
public event SimpleEventHandler SomethingHappened;
很簡單:event 是用來聲明事件的關鍵字,事件的型別總是由 delegate 指定。
2. 第一個簡單範例:事件「按下按鈕!」
步驟 1. 為事件處理器定義 delegate
// 事件處理器:不回傳、不接受參數
public delegate void ButtonClickHandler();
步驟 2. 含事件的類別
public class Button
{
// 事件:可以訂閱與取消訂閱,但無法從外部直接觸發
public event ButtonClickHandler Click;
// 模擬「按下按鈕」的方法
public void Press()
{
Console.WriteLine("按鈕被按下了!");
// 觸發事件:通知所有訂閱者
Click?.Invoke();
}
}
注意:事件是基於 delegate 聲明的,在方法 Press 裡面使用 ?.Invoke() 來呼叫事件。為什麼?因為事件可能沒有任何訂閱者,此時 Click 會是 null。安全呼叫運算子確保只有在有訂閱者時才會呼叫處理器。
步驟 3. 訂閱事件並執行程式
// 使用範例
public class Program
{
public static void Main()
{
var button = new Button();
// 加入「監聽者」(訂閱事件)
button.Click += OnButtonClicked;
button.Click += () => Console.WriteLine("又一個處理器!");
button.Press(); // 模擬按鈕被按下
// 可以取消訂閱事件
button.Click -= OnButtonClicked;
button.Press();
}
// 一般的事件處理器
public static void OnButtonClicked()
{
Console.WriteLine("處理器:按鈕被按下了!");
}
}
第一次按下會執行兩個處理器,第二次則只剩下那個 lambda。
3. 事件與 delegate 的差別
對 delegate,可以任意賦值甚至完全替換訂閱者清單——有人可能會寫類似 button.Click = null,這會把之前所有訂閱清掉。
事件則不一樣——它綁定到物件更嚴格。只有宣告該事件的類別的擁有者才能直接觸發事件(例如在類別內部呼叫 Click())。其他程式碼只能透過 += 或 -= 訂閱或取消訂閱,不能一次把它「清空」。這保護了封裝性,避免訂閱系統被「弄壞」。
4. 簽名:事件 delegate 的型別與參數
在 .NET 的慣例中:事件處理器會接收兩個參數—object sender(是誰觸發事件)與事件參數(EventArgs)。建議使用 delegate EventHandler 或 EventHandler<TEventArgs>。
範例:帶參數的 delegate
public delegate void ButtonClickHandler(object sender, EventArgs e);
EventArgs 是用來傳遞額外資訊的基底類別。需要更多資料時,就建立自己的子類別。
把它應用到我們的按鈕
// 事件參數類別
public class ButtonClickEventArgs : EventArgs
{
public string UserName { get; }
public ButtonClickEventArgs(string userName)
{
UserName = userName;
}
}
public class Button
{
public event EventHandler<ButtonClickEventArgs> Click;
public void Press(string userName)
{
Console.WriteLine("按鈕被按下了!");
Click?.Invoke(this, new ButtonClickEventArgs(userName));
}
}
public static void Main()
{
var button = new Button();
button.Click += OnButtonClicked;
button.Press("瓦西里");
}
public static void OnButtonClicked(object sender, ButtonClickEventArgs e)
{
Console.WriteLine($"使用者 {e.UserName} 按了按鈕!");
}
5. 視覺化示意:事件如何運作
+-------------+
| 使用者 |
+-------------+
|
v
+--------------+
| Button.Press|
+--------------+
|
v
+-----------------+ +------------------------+
| 呼叫 Click? |----->---| 訂閱者 1 |
+-----------------+ +------------------------+
| | 執行處理器 |
v +------------------------+
+-----------------+ +------------------------+
| 如果有訂閱者 |----->---| 訂閱者 2 |
+-----------------+ +------------------------+
: | 執行處理器 |
v +------------------------+
— Button.Press 觸發事件;訂閱者的處理器依序執行。
6. 有用的細節
.NET 中推薦的事件形式
使用 EventHandler 與 EventHandler<TEventArgs>,讓程式碼與函式庫和工具相容。這樣的做法可以讓事件演進更容易:你可以在自己的 EventArgs 加入新屬性而不會破壞訂閱者。
文件: EventHandler, event。
事件 vs delegate
如果只需要把一個元件綁到一個明確的方法——delegate 就足夠(delegate)。如果需要程式中不同部分能隨時訂閱或取消訂閱——就用事件(event)。
7. 常見錯誤與尷尬情況
錯誤 #1:在沒有檢查 null 的情況下呼叫事件。 若事件沒有訂閱者,直接呼叫會導致 NullReferenceException。使用安全呼叫:
Click?.Invoke(...);
錯誤 #2:嘗試從類別外部觸發事件。 事件只能在宣告它的類別內被觸發。從其他類別嘗試觸發會造成編譯錯誤——這是為了保護封裝性。
錯誤 #3:對同一個處理器重複訂閱。 如果同一個處理器被多次訂閱,會被呼叫多次。這是訂閱機制的特性。注意不要重複使用 +=,並正確使用 -= 取消訂閱。
GO TO FULL VERSION