委派是 C# 型別安全的函數指標,更是事件機制基石。本文透過「工人 Peter、壞老闆 Boss、宇宙 Universe」故事,展示從強型別耦合 → 介面 → 委派 → 事件 → 靜態監聽者 → 非同步的架構演進歷程。
class Worker
{
private Boss _boss;
public void Advise(Boss boss) => _boss = boss;
public void DoWork()
{
Console.WriteLine("Worker: work started");
_boss?.WorkStarted();
Console.WriteLine("Worker: work progressing");
_boss?.WorkProgressing();
Console.WriteLine("Worker: work completed");
if (_boss != null)
{
int grade = _boss.WorkCompleted();
Console.WriteLine($"Worker grade = {grade}");
}
}
}
class Boss
{
public void WorkStarted() { }
public void WorkProgressing() { }
public int WorkCompleted() => 2; // 打分 2/10
}
缺點:Worker 綁死 Boss 類別,無法擴充 Universe、無法單獨測試、違反開放封閉原則。
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 { ... } // 實作介面
進步:依賴抽象而非具體類別。缺點:介面粒度過粗,Boss 被迫實作不關心的方法 (介面隔離原則違反)。
將介面拆解為單一方法委派:
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("Worker: work started");
Started?.Invoke();
Console.WriteLine("Worker: work progressing");
Progressing?.Invoke();
Console.WriteLine("Worker: work completed");
if (Completed != null)
{
int grade = Completed(); // 僅取最後結果
Console.WriteLine($"Worker grade = {grade}");
}
}
}
class Boss
{
public int WorkCompleted() => 4; // 只關心完成評分
}
優點:Boss 只需提供關心的方法,委派即「單一方法介面」。
Universe 為單例/靜態類別,無實例:
class Universe
{
static void WorkerStartedWork() => Console.WriteLine("Universe notices...");
static int WorkerCompletedWork() => 7;
}
static void Main()
{
var peter = new Worker();
var boss = new Boss();
peter.Completed = boss.WorkCompleted; // 覆寫!
peter.Started = Universe.WorkerStartedWork; // 掛鉤靜態方法
peter.Completed = Universe.WorkerCompletedWork; // 再次覆寫,Boss 遺失!
}
問題:委派欄位為單一指派 (=),無法多播,後設定者覆蓋前者。
peter.Completed += boss.WorkCompleted; // += 累加
peter.Started += Universe.WorkerStartedWork;
peter.Completed += Universe.WorkerCompletedWork; // 現在兩者都能收到
class Worker
{
public event WorkStarted Started; // 編譯器產生 add/remove 存取子
public event WorkProgressing Progressing;
public event WorkCompleted Completed;
// 內部仍可直接 Invoke
protected virtual void OnCompleted()
{
Completed?.Invoke();
}
}
事件限制:外部只能 +=/-=,不能 = 覆寫、不能直接 Invoke()、不能清空清單。
Worker: work started
Universe notices worker starting work
Worker: work progressing
Worker: work completed
Better...
Universe pleased with worker's work
Worker grade = 7 // 僅顯示最後一個回傳值 (Universe 的 7)
Main: worker completed work
注意:多播委派有回傳值時,同步呼叫僅保留最後一個結果,這正是下集將解決的問題。
| 維度 | 介面 | 委派 |
|---|---|---|
| 方法數量 | 多個相關方法 | 單一方法 |
| 實作方 | 類別實作介面 | 任何符合簽名的方法 (實例/靜態/Lambda) |
| 多播 | 不支援 | 原生支援 (+=) |
| 狀態 | 實例擁有狀態 | 無狀態 (閉包除外) |
| 適用場景 | 契約、外掛架構、DI | 回調、事件、策略、非同步 |
強耦合 (Boss 類別)
↓ 抽離介面
介面 (IWorkerEvents) - 粒度太粗
↓ 拆解單一方法
委派欄位 (WorkStarted 等) - 無法多播
↓ += 多播
多播委派 - 外部可誤觸 (=, Invoke)
↓ event 封裝
事件 - 安全、慣用、支援標準模式
↓ 非同步模式 (下集)
APM / TAP
GetInvocationList()