.NET 將事件視為一等公民,語法優雅(btn.Click += Handler)。但在實務大型系統中,MulticastDelegate 內部以陣列儲存訂閱者,導致兩大限制:
本文分享兩種擴展模式:介面式優先級訂閱與雙向鏈表 + Token 模式,分別解決順序與效能問題。
參考 Java ActionListener 模式,定義介面 IInteractor,每個 UI 事件對應一個方法;發布者 InteractSource 維護 SortedList<int, IInteractor> 或 List<(int Priority, IInteractor)>,依優先級排序後依序呼叫。
public interface IInteractor
{
void OnMouseDown(MouseEventArgs e);
void OnMouseMove(MouseEventArgs e);
void OnMouseUp(MouseEventArgs e);
// ...
}
public class InteractSource
{
private readonly List<(int Priority, IInteractor Interactor)> _interactors = new();
public void AddInteractor(int priority, IInteractor interactor)
{
_interactors.Add((priority, interactor));
_interactors.Sort((a, b) => a.Priority.CompareTo(b.Priority)); // 數字小 = 優先級高
}
internal void DispatchMouseDown(MouseEventArgs e)
{
foreach (var (_, interactor) in _interactors)
interactor.OnMouseDown(e);
}
// 其他事件同理
}
優點:順序明確、跨模組可控、單元測試友善。缺點:需手動實作每個事件的 Dispatch 邏輯,無法自動對應 CLR 事件語法。
核心思想:訂閱時回傳包含鏈表節點參考的 Token,退訂時直接用節點 O(1) 移除。
public class EventSource
{
private readonly LinkedList<IEventListener> _list = new();
public Token AddListener(IEventListener listener)
{
var node = _list.AddLast(listener);
return new Token(node);
}
public void RemoveListener(Token token) => _list.Remove(token.Node);
public void RaiseEvent(EventArgs e)
{
foreach (var listener in _list) listener.OnEvent(e);
}
public class Token
{
internal LinkedListNode<IEventListener> Node { get; }
public Token(LinkedListNode<IEventListener> node) => Node = node;
}
}
public interface IEventListener { void OnEvent(EventArgs e); }
使用方式:
var token = source.AddListener(myListener);
// ...
source.RemoveListener(token); // O(1)
進階:Token 可再攜帶 EventSource 參考,退訂時自動驗證來源,防止「退訂從未訂閱的來源」Bug。
若同時需要順序與效能,可在鏈表節點中額外儲存 Priority,訂閱時依 Priority 尋找插入位置(O(N) 查找、O(1) 插入),退訂仍 O(1)。或改用 SortedDictionary<int, LinkedList<IEventListener>> 分級鏈表。
| 方案 | 順序保證 | 訂閱/退訂效能 | 適用場景 |
|---|---|---|---|
| MulticastDelegate (內建) | 不保證 | O(N) | 一般 UI、訂閱者<100 |
| 介面 + Priority | ✅ 明確 | O(N) 訂閱 / O(N) 退訂 | 需嚴格順序的管線、解釋器模式 |
| 雙向鏈表 + Token | 依插入順序 | O(1) / O(1) | 高頻動態訂閱/退訂、插件系統 |
| 混合方案 | ✅ 明確 | O(N) 訂閱 / O(1) 退訂 | 兩者皆需 |
若專案已引入 Rx,Subject<T>、Observable 內部同樣採用無鎖鏈表/不可變陣列,提供 Subscribe 回傳 IDisposable(相當於 Token),並支援 Priority 擴充套件,建議直接使用成熟程式庫而非自行實作。
.NET 內建事件機制適用於 90% 場景。當遭遇「順序敏感」或「大量動態訂閱」時,採用介面式優先級或Token + 雙向鏈表模式擴展,能以最小代價獲得確定性與高效能。理解背後資料結構(陣列 vs 鏈表)權衡,是架構師進階的關鍵一課。