事件是 C# 實現觀察者模式的核心語言特性,廣泛用於 UI 互動、非同步通知、領域事件等場景。本文深入剖析事件的底層原理、語法糖本質、進階用法與最佳實踐,幫助開發者從「會用」進階到「懂原理、能設計」。
// 委派宣告:定義方法簽章
public delegate void EventHandler(object sender, EventArgs e);
// 等同於編譯器產生的類別
public sealed class EventHandler : MulticastDelegate
{
public EventHandler(object @object, IntPtr method);
public virtual IAsyncResult BeginInvoke(object sender, EventArgs e, AsyncCallback callback, object object);
public virtual void EndInvoke(IAsyncResult result);
public virtual void Invoke(object sender, EventArgs e);
}
關鍵差異:
| 特性 | 委派欄位 | 事件 |
|---|---|---|
| 外部可直接呼叫 Invoke | 是 | 否 (僅類別內部可觸發) |
| 外部可直接指派 (=) | 是 | 否 (僅 += / -=) |
| 外部可清空委派鏈 | 是 | 否 |
| 封裝性 | 破壞封裝 | 保護發布者控制權 |
public class Button
{
// 編譯器產生:私有委派欄位 + 公開 add/remove 存取器
public event EventHandler Click;
protected virtual void OnClick(EventArgs e)
{
Click?.Invoke(this, e); // 安全觸發
}
}
public class Control : Component
{
// 靜態唯一鍵值,避免每個實例都配置委派欄位
static readonly object MouseDownEventKey = new object();
static readonly object MouseUpEventKey = new object();
// 內部儲存:EventHandlerList (鍵值對字典)
protected EventHandlerList Events { get; } = new EventHandlerList();
protected Delegate GetEventHandler(object key) => Events[key];
protected void SetEventHandler(object key, Delegate handler) => Events[key] = handler;
public event MouseEventHandler MouseDown
{
add => Events.AddHandler(MouseDownEventKey, value);
remove => Events.RemoveHandler(MouseDownEventKey, value);
}
public event MouseEventHandler MouseUp
{
add => Events.AddHandler(MouseUpEventKey, value);
remove => Events.RemoveHandler(MouseUpEventKey, value);
}
}
public event EventHandler MyEvent
{
// 外部 += 時呼叫
add
{
lock (this) // 或自訂同步
{
_myEvent += value;
}
}
// 外部 -= 時呼叫
remove
{
lock (this)
{
_myEvent -= value;
}
}
}
// 等價於欄位型事件的編譯器產生代碼
private EventHandler _myEvent;
public event EventHandler MyEvent
{
add => _myEvent += value;
remove => _myEvent -= value;
}
// 含資料的事件參數
public class PriceChangedEventArgs : EventArgs
{
public decimal OldPrice { get; }
public decimal NewPrice { get; }
public PriceChangedEventArgs(decimal oldPrice, decimal newPrice)
{
OldPrice = oldPrice; NewPrice = newPrice;
}
}
// 發布者
public class Stock
{
public event EventHandler PriceChanged;
decimal _price;
public decimal Price
{
get => _price;
set
{
if (_price != value)
{
var args = new PriceChangedEventArgs(_price, value);
_price = value;
OnPriceChanged(args);
}
}
}
protected virtual void OnPriceChanged(PriceChangedEventArgs e)
{
PriceChanged?.Invoke(this, e);
}
}
var stock = new Stock();
stock.PriceChanged += (sender, e) =>
{
Console.WriteLine($"價格變動: {e.OldPrice} → {e.NewPrice}");
};
stock.Price = 100; // 觸發事件
public interface INotifyPriceChanged
{
event EventHandler PriceChanged;
}
// 明確實作介面事件
public class Stock : INotifyPriceChanged
{
event EventHandler INotifyPriceChanged.PriceChanged
{
add => PriceChanged += value;
remove => PriceChanged -= value;
}
public event EventHandler PriceChanged;
// ...
}
public static class ApplicationEvents
{
public static event EventHandler ApplicationExit;
internal static void OnApplicationExit()
{
ApplicationExit?.Invoke(null, EventArgs.Empty);
}
}
// 訂閱需注意:靜態事件生命週期 = 應用程式域,易造成記憶體洩漏
ApplicationEvents.ApplicationExit += OnExit;
// 必須在適當時機 -= 移除
public class Base
{
public virtual event EventHandler MyEvent;
protected virtual void OnMyEvent(MyEventArgs e) => MyEvent?.Invoke(this, e);
}
public class Derived : Base
{
// 子類別可 override OnMyEvent 擴充行為
protected override void OnMyEvent(MyEventArgs e)
{
// 子類別邏輯...
base.OnMyEvent(e); // 呼叫基類觸發
}
}
// 避免單一訂閱者阻塞其他訂閱者
protected virtual async Task OnEventAsync(MyEventArgs e)
{
var handlers = MyEvent?.GetInvocationList();
if (handlers == null) return;
var tasks = handlers
.Cast>()
.Select(h => Task.Run(() => h(this, e)));
await Task.WhenAll(tasks);
}
public class Publisher
{
public event EventHandler MyEvent;
}
public class Subscriber
{
public Subscriber(Publisher p)
{
p.MyEvent += OnEvent; // Publisher 持有 Subscriber 參考
}
void OnEvent(object s, EventArgs e) { }
}
// 使用後若不 p.MyEvent -= subscriber.OnEvent,Subscriber 永不被回收
IDisposable,在 Dispose 中 -=WeakReference 或 WeakEventManager (WPF)
// 弱事件包裝器簡易實作
public class WeakEvent where TEventArgs : EventArgs
{
private readonly List>> _handlers = new();
public void Add(EventHandler handler)
{
_handlers.Add(new WeakReference>(handler));
}
public void Remove(EventHandler handler)
{
_handlers.RemoveAll(wr => !wr.TryGetTarget(out var h) || h == handler);
}
public void Raise(object sender, TEventArgs e)
{
_handlers.RemoveAll(wr => !wr.TryGetTarget(out _)); // 清理已死參考
foreach (var wr in _handlers)
{
if (wr.TryGetTarget(out var handler))
handler(sender, e);
}
}
}
| 場景 | 建議 |
|---|---|
| 大量事件、少數訂閱 | 使用 EventHandlerList (屬性型事件) |
| 高頻觸發 (如滑鼠移動) | 節流/去抖、合併事件引數、避免配置物件 |
| 跨執行緒觸發 | 使用 SynchronizationContext 或 Task.Run 切換上下文 |
| 熱路徑 | 考慮 ValueTask、避免委派鏈過長 |
| 模式 | 優點 | 缺點 | 適用場景 |
|---|---|---|---|
| C# 事件 | 語言原生、型別安全、IDE 支援好 | 強參考易洩漏、靜態難管理 | UI、元件內部通知 |
| System.Reactive (Rx.NET) | 可組合、LINQ 查詢、背壓控制 | 學習曲線陡、依賴外部套件 | 複雜事件流、串流處理 |
| MediatR / 領域事件 | 解耦、單一職責、測試友善 | 間接層增加、除錯較難 | DDD、微服務、CQRS |
| Channel / Dataflow | 高效能、生產消費者、非同步原生 | 非事件語意、需手動管理 | 高併發管道、背景處理 |
事件本質是 受限的多播委派,透過 add/remove 存取器保護發布者的觸發控制權。掌握 標準模式、記憶體管理、進階用法,能設計出健壯、可維護的事件驅動架構。現代開發中,視複雜度選擇:簡單場景用原生事件,複雜流程用 Rx.NET/MediatR,高效能管道用 Channel。核心原則:發布者決定何時觸發,訂閱者決定如何回應,雙方鬆耦合。