事件是 C# 實現觀察者模式的核心機制,讓物件間能鬆耦合通訊。本文從語言規格、IL 層面、編譯器實作細節,深入剖析事件的聲明、訂閱、引發流程,並探討自訂事件存取器、非同步事件模式等進階主題。
public class Button : Control
{
// 欄位型事件:編譯器自動產生私有委派欄位 + add/remove 存取器
public event EventHandler Click;
}
當事件數量龐大但多數未訂閱時,使用事件屬性節省記憶體:
public class Control : Component
{
// 靜態唯一鍵值
static readonly object MouseDownEventKey = new object();
static readonly object MouseUpEventKey = new object();
// 內部儲存機制:EventHandlerList (鍵值對集合)
protected Delegate GetEventHandler(object key) => _events[key];
protected void SetEventHandler(object key, Delegate handler) => _events[key] = handler;
// 事件屬性:自訂 add/remove
public event MouseEventHandler MouseDown
{
add => SetEventHandler(MouseDownEventKey, value);
remove => SetEventHandler(MouseDownEventKey, value);
}
public event MouseEventHandler MouseUp
{
add => SetEventHandler(MouseUpEventKey, value);
remove => SetEventHandler(MouseUpEventKey, value);
}
}
這就是 WinForms Control 實際採用的模式,EventHandlerList 內部用單向鏈表儲存,未訂閱事件不佔用額外記憶體。
原始碼:
public event EventHandler Click;
編譯為 IL 相當於:
.class public Button extends Control
{
.field private class [mscorlib]System.EventHandler Click
.event System.EventHandler Click
{
.addon instance void Button::add_Click(class System.EventHandler)
.removeon instance void Button::remove_Click(class System.EventHandler)
}
.method public hidebysig specialname instance void add_Click(class System.EventHandler value) { ... }
.method public hidebysig specialname instance void remove_Click(class System.EventHandler value) { ... }
}
編譯器自動產生:
Clickadd_Click/remove_Click 方法(使用 Delegate.Combine/Remove)protected virtual void OnClick(EventArgs e)
{
Click?.Invoke(this, e); // C# 6+ null 條件運算子,等同 if (Click != null) Click(this, e);
}
關鍵點:
Invoke 方法On+EventName 受保護虛擬方法,方便衍生類別擴充var handler = Click; handler?.Invoke(this, e);在宣告事件的類別外部,只能使用 += / -=,無法直接讀取、指派或呼叫委派:
Button btn = new Button();
// 合法
btn.Click += Btn_Click;
btn.Click -= Btn_Click;
// 編譯錯誤
// var d = btn.Click; // 無法讀取
// btn.Click = null; // 無法指派
// btn.Click(this, EventArgs.Empty); // 無法直接呼叫
這保護了封裝性,防止外部代碼竄改調用清單。
在類別內部,事件表現為委派欄位,可直接檢查、呼叫、清空:
public void Reset() => Click = null; // 僅限類別內部
傳統 BeginInvoke/EndInvoke 已過時,現代採用 Task-based Asynchronous Pattern (TAP):
public event Func
防止事件訂閱導致記憶體洩漏(發布者持有訂閱者強參考):
// 使用 WeakReference 包裝委派
public class WeakEventManager where TEventArgs : EventArgs
{
private readonly List>> _handlers = new();
public void AddHandler(EventHandler handler)
=> _handlers.Add(new WeakReference>(handler));
public void RemoveHandler(EventHandler handler)
{
for (int i = _handlers.Count - 1; i >= 0; i--)
{
if (_handlers[i].TryGetTarget(out var h) && h == handler)
_handlers.RemoveAt(i);
}
}
public void Raise(object sender, TEventArgs e)
{
for (int i = _handlers.Count - 1; i >= 0; i--)
{
if (_handlers[i].TryGetTarget(out var h))
h(sender, e);
else
_handlers.RemoveAt(i); // 清理失效引用
}
}
}
介面可宣告事件,實作類別必須提供對應事件:
public interface INotifyDataChanged
{
event EventHandler DataChanged;
}
public class DataService : INotifyDataChanged
{
public event EventHandler DataChanged;
// ...
}
base())注意:基底類別建構子執行時,衍生類別欄位尚未初始化。若基底類別在建構子中引發虛擬事件,衍生類別覆寫方法存取自身欄位會看到預設值 (null/0)。
C# 解構子 (~ClassName()) 對應 Finalize 方法,由 GC 執行緒呼叫。不要在解構子中取消訂閱事件(可能已被 GC),應實作 IDisposable 並在 Dispose() 中清理:
public class Subscriber : IDisposable
{
private readonly Publisher _publisher;
public Subscriber(Publisher p) { _publisher = p; _publisher.Event += Handler; }
public void Dispose() => _publisher.Event -= Handler;
private void Handler(object s, EventArgs e) { }
}
C# 事件是委派的語言級封裝,提供發布/訂閱機制並強制封裝性。核心要點:
?.Invoke() 並考慮執行緒安全+=/-=,內部可直接操作委派Task 回傳型委派IDisposable 防洩漏理解編譯器實作細節,能寫出更健壯、高效能的事件驅動程式碼。