.NET 事件 (event) 是觀察者模式的語言級實作,但許多開發者對 delegate、event、多播、sender/EventArgs 的關係感到困惑。本文從 Java 事件監聽器 對比切入,層層遞進重構出 .NET 事件完整模型,並釐清多播調用順序、執行緒安全、自訂 EventArgs 等實戰細節。
Java 無 delegate,事件監聽器即介面:
// Java 風格 (C# 模擬)
interface ActionListener { void ActionPerformed(); }
class MyButton
{
private ActionListener _listener;
public void SetActionListener(ActionListener l) => _listener = l;
public void ClickMe() => _listener?.ActionPerformed();
}
// 使用:建立實作類別、實例化、註冊
class MyListener : ActionListener
{
public void ActionPerformed() => Console.WriteLine("Button Clicked!");
}
var btn = new MyButton();
btn.SetActionListener(new MyListener());
btn.ClickMe();
缺點:每個事件需定義一介面、一實作類別,樣板代碼極多。
delegate 是「方法簽章的型別安全封裝」,可視為「函數指標」的物件導向版:
delegate void OnClick(); // 定義無參無回傳的方法簽章型別
class MyButton
{
private OnClick _click; // 委派欄位
public void SetOnClick(OnClick oc) => _click = oc;
public void ClickMe() => _click?.Invoke();
}
// 使用:匿名方法 / Lambda 直接傳遞
var btn = new MyButton();
btn.SetOnClick(() => Console.WriteLine("Button Clicked!"));
btn.ClickMe();
優點:無需額外介面/類別,內嵌邏輯即可註冊。
Delegate 本身支援鏈結 (+/-):
class MyButton
{
private List _clicks = new List();
public void AddOnClick(OnClick oc) => _clicks.Add(oc);
public void ClickMe() => _clicks.ForEach(c => c?.Invoke());
}
// 註冊多個
btn.AddOnClick(() => Console.WriteLine("First"));
btn.AddOnClick(() => Console.WriteLine("Second"));
btn.ClickMe();
// 輸出:First → Second (註冊順序 = 執行順序)
event 限制外部只能 +=/-=,不能直接 = 覆寫或 Invoke():
class MyButton
{
public event OnClick Click; // 編譯器自動產生 add_Click/remove_Click + 私有委派欄位
public void ClickMe() => Click?.Invoke(this, EventArgs.Empty); // 標準模式傳 sender
}
var btn = new MyButton();
btn.Click += (s, e) => Console.WriteLine("First");
btn.Click += (s, e) => Console.WriteLine("Second");
btn.ClickMe();
event vs delegate 欄位:event 禁止外部 `Click = null` 或 `Click()`,保護封裝性。
.NET Framework 約定:事件處理器簽章為 void Handler(object sender, TEventArgs e)
// 1. 自訂 EventArgs 攜帶資料
class MyClickEventArgs : EventArgs
{
public string Message { get; set; }
public DateTime ClickTime { get; } = DateTime.Now;
}
// 2. 定義委派型別 (慣用 EventHandler 泛型)
delegate void ClickEventHandler(object sender, MyClickEventArgs e);
// 3. 發布者
class MyButton
{
public event ClickEventHandler Click;
public string Text { get; set; }
public void ClickMe()
{
var args = new MyClickEventArgs { Message = "User clicked!" };
Click?.Invoke(this, args); // this = sender
}
}
// 4. 訂閱者
var btn = new MyButton { Text = "OK" };
btn.Click += (sender, e) =>
{
var b = (MyButton)sender;
Console.WriteLine($"{b.Text}: {e.Message} at {e.ClickTime}");
};
btn.ClickMe();
WinForms 控制項常見 MouseDown → Click → MouseUp 順序,實作方式為 在同一方法中依序 Invoke 多個事件:
class MyButton
{
public event ClickEventHandler BeforeClick;
public event ClickEventHandler Click;
public event ClickEventHandler AfterClick;
public void ClickMe()
{
var args = new MyClickEventArgs();
args.Message = "Before Click"; BeforeClick?.Invoke(this, args);
args.Message = "On Click"; Click?.Invoke(this, args);
args.Message = "After Click"; AfterClick?.Invoke(this, args);
}
}
// 共用單一處理器
void Handle(object s, MyClickEventArgs e)
=> Console.WriteLine($"{e.Message}");
btn.BeforeClick += Handle;
btn.Click += Handle;
btn.AfterClick += Handle;
btn.ClickMe();
// 輸出順序固定:Before → On → After
每次新建 EventArgs:避免多執行緒下共用物件導致資料競爭;語意上每次事件參數應獨立。
// 無需自訂 delegate,直接使用框架內建
public event EventHandler Click;
// 等同於:delegate void EventHandler(object sender, T e);
private EventHandler _click;
public event EventHandler Click
{
add
{
lock(_sync) _click += value; // 執行緒安全註冊
// 可加入日誌、驗證、弱引用包裝等
}
remove => lock(_sync) _click -= value;
}
訂閱者若未明確 `-=`,發布者持有強引用導致無法 GC。可用 WeakReference 包裝委派:
// 簡易 WeakEventManager 概念
class WeakEvent where T : class
{
private List _refs = new List();
public void Add(T handler) => _refs.Add(new WeakReference(handler));
public void Remove(T handler) => _refs.RemoveAll(r => !r.IsAlive || r.Target == handler);
public void Raise(object sender, EventArgs e)
{
foreach (var r in _refs.Where(r => r.IsAlive))
(r.Target as Delegate)?.DynamicInvoke(sender, e);
}
}
public async void ClickMeAsync()
{
var args = new MyClickEventArgs();
var tasks = Click?.GetInvocationList()
.Cast>()
.Select(d => d(this, args)) ?? Enumerable.Empty();
await Task.WhenAll(tasks); // 等待所有非同步處理器完成
}
| 誤區 | 正確做法 |
|---|---|
| 直接用 public delegate 欄位 | 改用 event 封裝,防止外部覆寫/Invoke |
| EventArgs 重複使用 (修改屬性後再次 Invoke) | 每次 Invoke 新建實例,保證執行緒安全與語意正確 |
| 同步事件中執行耗時 I/O | 改非同步事件或背景工作,避免阻塞 UI/發布者 |
| 未實作 IDisposable 移除訂閱 | Dispose 時 `-=` 所有事件,或用弱引用 |
| 靜態事件導致單例洩漏 | 謹慎使用靜態事件,必須提供明確清理機制 |
delegate (方法簽章型別)
↓ 封裝為
event (add/remove 存取器 + 私有委派欄位)
↓ 多播鏈結
InvocationList (Delegate[])
↓ 依序調用
handler1(sender, args) → handler2(sender, args) → ...
↓ 標準模式
sender (object) + EventArgs (資料承載)
↓ 擴展
自訂 EventArgs / 弱引用 / 非同步 / 自訂存取器