委派是 C# 的核心機制,封裝方法參考,支援型別安全、多播、非同步呼叫。類似 C++ 函式指標,但更安全、更強大。
// 宣告:無參數、回傳 string
public delegate string GreetingDelegate();
// 目標方法
public class Greeter
{
public string SayHello() => "Hello, World!";
public string SayGoodbye() => "Goodbye!";
}
// 使用
var greeter = new Greeter();
GreetingDelegate d1 = greeter.SayHello;
GreetingDelegate d2 = greeter.SayGoodbye;
Console.WriteLine(d1()); // Hello, World!
Console.WriteLine(d2()); // Goodbye!
委派可透過 `+` / `-` 運算子組合/移除,形成調用清單:
public delegate void NotifyDelegate(string message);
public class Notifier
{
public void Email(string msg) => Console.WriteLine($"Email: {msg}");
public void Sms(string msg) => Console.WriteLine($"SMS: {msg}");
public void Push(string msg) => Console.WriteLine($"Push: {msg}");
}
var notifier = new Notifier();
NotifyDelegate multicast = null;
multicast += notifier.Email;
multicast += notifier.Sms;
multicast += notifier.Push;
// 呼叫一次,依序執行三個方法
multicast("系統維護通知");
// 輸出:
// Email: 系統維護通知
// SMS: 系統維護通知
// Push: 系統維護通知
// 移除某個方法
multicast -= notifier.Sms;
multicast("第二次通知");
// 僅執行 Email 與 Push
事件提供封裝層,限制外部只能 `+=`/`-=` 訂閱/取消訂閱,不能直接呼叫或清空調用清單。
// 1. 定義事件委派型別(慣用 EventHandler 模式)
public delegate void PriceChangedEventHandler(decimal oldPrice, decimal newPrice);
public class Stock
{
private decimal _price;
// 2. 宣告事件
public event PriceChangedEventHandler PriceChanged;
public decimal Price
{
get => _price;
set
{
if (_price != value)
{
decimal old = _price;
_price = value;
// 3. 觸發事件(檢查 null,避免無訂閱者時擲出 NullReferenceException)
PriceChanged?.Invoke(old, value);
}
}
}
}
// 4. 訂閱與處理
var stock = new Stock();
stock.PriceChanged += (old, @new) =>
Console.WriteLine($"價格變動: {old:C} → {@new:C}");
stock.Price = 100.50m; // 觸發事件
stock.Price = 101.20m; // 再次觸發
.NET 標準化事件簽名:void EventHandler(object sender, TEventArgs e)
// 自訂事件參數
public class PriceChangedEventArgs : EventArgs
{
public decimal OldPrice { get; }
public decimal NewPrice { get; }
public DateTime ChangeTime { get; }
public PriceChangedEventArgs(decimal oldPrice, decimal newPrice)
{
OldPrice = oldPrice;
NewPrice = newPrice;
ChangeTime = DateTime.UtcNow;
}
}
// 發布者
public class StockV2
{
private decimal _price;
public event EventHandler PriceChanged;
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 StockV2();
stock.PriceChanged += (sender, e) =>
{
Console.WriteLine($"[{e.ChangeTime:HH:mm:ss}] {((StockV2)sender).GetType().Name}: {e.OldPrice:C} → {e.NewPrice:C}");
};
private EventHandler _priceChanged;
public event EventHandler PriceChanged
{
add
{
lock (_lock) // 執行緒安全
{
_priceChanged += value;
}
}
remove
{
lock (_lock)
{
_priceChanged -= value;
}
}
}
訂閱者若未取消訂閱,發布者持有強參考導致無法 GC。解決方案:
WeakReference + 自訂事件存取器WeakEventManagerMicrosoft.Toolkit.HighPerformance.WeakEventListener)// 同步呼叫(預設):逐一執行,阻塞發布者
PriceChanged?.Invoke(this, e);
// 非同步並行呼叫(需自行實作)
foreach (var handler in PriceChanged?.GetInvocationList() ?? Array.Empty())
{
Task.Run(() => handler.DynamicInvoke(this, e));
}
// 注意:例外處理、順序保證、完成等待需自行設計
public interface IObserver
{
void OnNext(T value);
void OnError(Exception error);
void OnCompleted();
}
public class Subject
{
private readonly List> _observers = new();
private readonly object _lock = new();
public IDisposable Subscribe(IObserver observer)
{
lock (_lock) _observers.Add(observer);
return new Unsubscriber(_observers, observer);
}
public void Notify(T value)
{
IObserver[] snapshot;
lock (_lock) snapshot = _observers.ToArray();
foreach (var obs in snapshot) obs.OnNext(value);
}
private class Unsubscriber : IDisposable
{
private readonly List> _list;
private readonly IObserver _observer;
public Unsubscriber(List> list, IObserver observer) { _list = list; _observer = observer; }
public void Dispose() { lock (_list) _list.Remove(_observer); }
}
}
public static class DomainEvents
{
private static readonly List _handlers = new();
private static readonly object _lock = new();
public static void Register(Action handler) where T : IDomainEvent
{
lock (_lock) _handlers.Add(handler);
}
public static void Raise(T @event) where T : IDomainEvent
{
Delegate[] snapshot;
lock (_lock) snapshot = _handlers.ToArray();
foreach (var h in snapshot.OfType>()) h(@event);
}
}
public interface IDomainEvent { }
public class OrderPlacedEvent : IDomainEvent { public int OrderId { get; set; } }
// 使用
DomainEvents.Register(e => SendConfirmationEmail(e.OrderId));
DomainEvents.Raise(new OrderPlacedEvent { OrderId = 123 });
| 陷阱 | 後果 | 解決方案 |
|---|---|---|
| 事件未取消訂閱 | 記憶體洩漏(發布者持有訂閱者參考) | 實作 IDisposable、使用弱事件、或在 Unload 時清理 |
| 多執行緒並發修改事件 | 調用清單競態條件 | 事件存取器加 lock、或用 ImmutableList |
| 事件處理器拋出例外 | 後續處理器不執行、發布者崩潰 | 每個處理器內部 try/catch、或自訂事件存取器捕獲 |
| 在建構函式中訂閱靜態事件 | 物件永遠無法被 GC(靜態事件存活到進程結束) | 避免靜態事件、或提供明確 Unregister 方法 |
| 委派回傳值被忽略 | 邏輯錯誤(以為所有回傳值都會用到) | 多播委派勿依賴回傳值、改用 ref/out 參數或事件參數回傳 |
委派是 C# 函式式程式設計的基石,事件則是建立鬆耦合、可擴充架構的關鍵。掌握多播行為、標準事件模式、執行緒安全、弱事件、非同步呼叫等進階技巧,並避開常見陷阱,即能設計出健壯、優雅的事件驅動系統。