想像一個場景:勤勞的工人彼得,吝嗇且多疑的老闆。老闆想隨時掌握進度,彼得卻不想被盯著。這個「通知對方」的需求,正是委派模式誕生的緣起。本文以故事形式,層層剖析從硬編碼耦合 → 介面 → 委派 → 事件 → 非同步模式的演進邏輯。
class Worker
{
private Boss _boss;
public void Advise(Boss boss) => _boss = boss;
public void DoWork()
{
Console.WriteLine("工作: 開始");
_boss?.WorkStarted();
Console.WriteLine("工作: 進行中");
_boss?.WorkProgressing();
Console.WriteLine("工作: 完成");
if (_boss != null)
{
int grade = _boss.WorkCompleted();
Console.WriteLine($"工人得分:{grade}");
}
}
}
class Boss
{
public void WorkStarted() { /* 不關心 */ }
public void WorkProgressing() { /* 不關心 */ }
public int WorkCompleted() => 2; // 總分 10 分
}
缺點:Worker 強依賴 Boss 具體類別,無法替換、無法多播、違反開放封閉原則。
interface IWorkerEvents
{
void WorkStarted();
void WorkProgressing();
int WorkCompleted();
}
class Worker
{
private IWorkerEvents _events;
public void Advise(IWorkerEvents events) => _events = events;
// DoWork 內呼叫 _events.xxx()
}
class Boss : IWorkerEvents { /* 實作介面 */ }
class Universe : IWorkerEvents { /* 另一個監聽者 */ }
進步:解耦具體類別,支援多型。
痛點:介面粒度過粗,Boss 被迫實作不關心的方法(介面隔離原則違反);單一 Worker 只能掛一組監聽者(_events 為單一參考)。
delegate void WorkStarted();
delegate void WorkProgressing();
delegate int WorkCompleted();
class Worker
{
public WorkStarted Started;
public WorkProgressing Progressing;
public WorkCompleted Completed;
public void DoWork()
{
Console.WriteLine("工作: 開始");
Started?.Invoke();
Console.WriteLine("工作: 進行中");
Progressing?.Invoke();
Console.WriteLine("工作: 完成");
if (Completed != null)
{
int grade = Completed.Invoke();
Console.WriteLine($"工人得分:{grade}");
}
}
}
class Boss
{
public int WorkCompleted() { Console.WriteLine("Better..."); return 4; }
}
class Universe
{
static void WorkerStartedWork() => Console.WriteLine("宇宙注意到工人開始工作");
static int WorkerCompletedWork() { Console.WriteLine("宇宙對工人工作感到欣慰"); return 7; }
}
// 使用
var peter = new Worker();
var boss = new Boss();
peter.Completed = new WorkCompleted(boss.WorkCompleted); // 實例方法
peter.Started = new WorkStarted(Universe.WorkerStartedWork); // 靜態方法
關鍵突破:
委派欄位若為 public,外部可任意覆寫 peter.Completed = null 或直接 peter.Completed() 竊取觸發權。event 關鍵字為委派加上「屬性式」存取器,僅允許 += / -=。
class Worker
{
public event WorkStarted Started;
public event WorkProgressing Progressing;
public event WorkCompleted Completed;
// DoWork 內部仍可直接 Invoke()
}
編譯器自動產生 add_Started/remove_Started 方法,保護調用清單不被外部清空或盜用。
多個監聽者都回傳分數時,預設只取最後一個。需手動遍歷 GetInvocationList():
if (Completed != null)
{
foreach (WorkCompleted wc in Completed.GetInvocationList())
{
int grade = wc.Invoke();
Console.WriteLine($"收到分數:{grade}");
}
}
當監聽者處理耗時(如寫入資料庫、呼叫 Web API),同步 Invoke 會卡住 Worker。三種非同步策略:
foreach (var wc in Completed.GetInvocationList())
wc.BeginInvoke(null, null); // 執行緒池執行,結果丟棄
foreach (var wc in Completed.GetInvocationList())
{
var ar = wc.BeginInvoke(null, null);
while (!ar.IsCompleted) Thread.Sleep(1); // 占用 CPU,不推薦
int grade = wc.EndInvoke(ar);
}
foreach (var wc in Completed.GetInvocationList())
wc.BeginInvoke(WorkGraded, wc); // 完成時呼叫 WorkGraded
void WorkGraded(IAsyncResult ar)
{
var wc = (WorkCompleted)ar.AsyncState;
int grade = wc.EndInvoke(ar);
Console.WriteLine($"非同步收到分數:{grade}");
}
注意:回調在執行緒池執行緒執行,若涉及 UI 更新需切回 UI 執行緒(Control.BeginInvoke / Dispatcher.Invoke)。
自 .NET 4.5 起,Task 與 async/await 取代了 APM(Begin/End)模式:
// 將委派包裝為 Task
Func> asyncWork = () => Task.Run(() => boss.WorkCompleted());
// 並行等待所有
var tasks = Completed.GetInvocationList()
.Cast()
.Select(d => Task.Run(() => d()));
var grades = await Task.WhenAll(tasks);
優點:例外處理統一、可取消、進度報告、避免回調地獄。
從彼得的故事看委派演進:
理解這條演進脈絡,不僅能寫出更優雅的事件驅動程式碼,更能深刻體會 .NET 設計哲學的核心——「關注點分離」與「控制反轉」。