委派與事件是 C# 實作「觀察者模式」與「策略模式」的核心語言機制。本文以經典《C# 高級編程》範例——泡沫排序委派回呼——為切入點,層層深入至事件標準模式、EventHandler<T>、弱事件、非同步事件、來源產生器與現代化最佳實務。
// 委派定義:比較兩個物件,回傳 true 表示右邊大於左邊
public delegate bool CompareOp(object lhs, object rhs);
// 策略實作:依薪資比較 Employee
public class Employee
{
public string Name { get; }
public decimal Salary { get; }
public Employee(string name, decimal salary) => (Name, Salary) = (name, salary);
public override string ToString() => $"{Name}, {Salary:C}";
// 靜態方法符合 CompareOp 簽名
public static bool RhsIsGreater(object lhs, object rhs)
=> ((Employee)rhs).Salary > ((Employee)lhs).Salary;
}
// 演算法類別:接受委派作為比較策略
public static class BubbleSorter
{
public static void Sort(object[] array, CompareOp comparer)
{
for (int i = 0; i < array.Length; i++)
for (int j = i + 1; j < array.Length; j++)
if (comparer(array[j], array[i])) // 委派呼叫
(array[i], array[j]) = (array[j], array[i]); // 交換
}
}
// 使用
var employees = new object[]
{
new Employee("Karli", 20000),
new Employee("Bill", 10000),
new Employee("Simon", 25000)
};
CompareOp cmp = new CompareOp(Employee.RhsIsGreater); // 建立委派實例
BubbleSorter.Sort(employees, cmp); // 注入策略
方法指標 + 目標物件 (this)。cmp += AnotherMethod 可串聯多個方法,依序呼叫。Func<in T, out TResult> 允許衍生類別替換基底類別。
public class TemperatureMonitor
{
// 1. 定義事件資料類別 (繼承 EventArgs)
public class TemperatureChangedEventArgs : EventArgs
{
public decimal Temperature { get; }
public DateTime Timestamp { get; }
public TemperatureChangedEventArgs(decimal temp)
=> (Temperature, Timestamp) = (temp, DateTime.UtcNow);
}
// 2. 定義委派類型 (慣用 EventHandler)
public event EventHandler TemperatureChanged;
// 3. 受保護虛擬方法觸發事件 (供衍生類別覆寫)
protected virtual void OnTemperatureChanged(TemperatureChangedEventArgs e)
{
// 執行緒安全:複製委派參考再檢查 null
TemperatureChanged?.Invoke(this, e);
}
// 4. 業務邏輯
private decimal _currentTemp;
public decimal CurrentTemp
{
get => _currentTemp;
set
{
if (_currentTemp != value)
{
_currentTemp = value;
OnTemperatureChanged(new TemperatureChangedEventArgs(value));
}
}
}
}
// 訂閱者
var monitor = new TemperatureMonitor();
monitor.TemperatureChanged += (sender, e) =>
Console.WriteLine($"[{e.Timestamp:O}] Temp: {e.Temperature}°C");
monitor.CurrentTemp = 25.5m; // 觸發事件
add/remove) 自訂儲存
// 大量事件時用 EventHandlerList 儲存 (WinForms 元件模式)
public class ComponentBase
{
private readonly EventHandlerList _events = new();
private static readonly object _clickKey = new();
public event EventHandler Click
{
add => _events.AddHandler(_clickKey, value);
remove => _events.RemoveHandler(_clickKey, value);
}
protected void OnClick(EventArgs e)
=> (_events[_clickKey] as EventHandler)?.Invoke(this, e);
}
// .NET 4.5+ WeakEventManager (WPF 風格)
public class WeakTemperatureMonitor
{
public event EventHandler TemperatureChanged
{
add => WeakEventManager
.AddHandler(this, "TemperatureChanged", value);
remove => WeakEventManager
.RemoveHandler(this, "TemperatureChanged", value);
}
// ... 觸發邏輯同前
}
// 或自訂 WeakDelegate (通用)
public static class WeakEvent
{
public static void Subscribe(object source, string eventName,
EventHandler handler) where TEventArgs : EventArgs
{
var weakRef = new WeakReference(handler);
EventHandler proxy = (s, e) =>
{
if (weakRef.Target is EventHandler h) h(s, e);
else WeakEventManager.Detach(source, eventName, proxy);
};
// 使用反射或來源產生器掛鉤 proxy
}
}
// 問題:同步事件阻塞發布者,異步事件例外處理困難
// 方案 A:發布者火後不顧
public async Task RaiseAsync(TemperatureChangedEventArgs e)
{
var tasks = TemperatureChanged?.GetInvocationList()
.Cast>()
.Select(h => Task.Run(() => h(this, e))) ?? Enumerable.Empty();
await Task.WhenAll(tasks);
}
// 方案 B:Channel / MediatR / Event Bus (架構層級解耦)
Func/Action 取代自訂委派
// 泡沫排序現代寫法
public static void Sort(T[] array, Func comparer)
{
for (int i = 0; i < array.Length; i++)
for (int j = i + 1; j < array.Length; j++)
if (comparer(array[j], array[i]))
(array[i], array[j]) = (array[j], array[i]);
}
// 使用
BubbleSorter.Sort(employees, (a, b) => a.Salary > b.Salary);
// 套件: Microsoft.CodeAnalysis.CSharp.SourceGenerators
// 定義 partial 類別與屬性
[GenerateEventArgs]
public partial class OrderService
{
public event EventHandler OrderCreated;
// 產生器自動建立 OnOrderCreated、EventArgs 類別等
}
| 錯誤 | 後果 | 修正 |
|---|---|---|
直接呼叫 Event(args) 不檢查 null | 無訂閱時拋 NullReferenceException | Event?.Invoke(this, args) |
| 在建構函式訂閱靜態事件,未取消訂閱 | 記憶體洩漏 (靜態事件持有實例參考) | 實作 IDisposable、WeakEvent |
| 事件處理器拋例外中斷後續處理器 | 部分訂閱者未執行 | 每個處理器包 try/catch、或用 GetInvocationList 逐一 try |
| 在迴圈中用 lambda 訂閱事件捕捉迴圈變數 | 所有處理器共用同一變數 (閉包陷阱) | 區域變數複製: var i = index; ev += (s,e)=>Use(i); |
| 事件資料類別可變 | 訂閱者竄改資料影響後續處理器 | EventArgs 設計為不可變 (init-only/setter 私有) |
delegate 關鍵字、多播委派、事件關鍵字。EventHandler<T>、協變/逆變雛形。Func/Action 通用委派。in/out)、動態繫結。?.Invoke null 條件運算子。ref 回傳、區域函式。record EventArgs、init only 屬性、來源產生器。從 CompareOp 委派注入排序策略,到 EventHandler<T> 標準化觀察者模式,再到 Func<T,T,bool> 函數式風格與架構級事件總線,委派與事件見證了 C# 從物件導向走向函數式、從單體走向微服務的演進。掌握其類型安全、多播語意、生命週期管理、執行緒同步四大核心,才能在任何架構層級駕馭「反轉控制」的強大威力。