委託是 C# 異步編程的起點。本文層層遞進:多播委託返回值陷阱 → APM 模式 (BeginInvoke/EndInvoke) 三種調用方式 → 現代 Task/async-await 統一模型,並對比優劣。
delegate int BinaryOp(int x, int y);
static int Add(int x, int y) => x + y;
static int Sub(int x, int y) => x - y;
static void Main() {
BinaryOp op = Add;
op += Sub; // 多播
Console.WriteLine(op(3, 2)); // 輸出 1 (僅最後一個方法的返回值)
}
原因:CLR 依次調用鏈中每個委託,但只保留最後一次調用的返回值。
獲取所有返回值:
foreach (BinaryOp single in op.GetInvocationList()) {
Console.WriteLine(single(3, 2)); // 依次輸出 5, 1
}
委託天生支持 BeginInvoke/EndInvoke,由線程池執行。
BinaryOp op = Add;
IAsyncResult ar = op.BeginInvoke(3, 2, null, null);
while (!ar.IsCompleted) {
Console.WriteLine("Working...");
Thread.Sleep(100);
}
int result = op.EndInvoke(ar);
Console.WriteLine($"Result: {result}");
缺點:主線程忙等待,浪費 CPU。
IAsyncResult ar = op.BeginInvoke(3, 2, null, null);
ar.AsyncWaitHandle.WaitOne(); // 阻塞直到完成
int result = op.EndInvoke(ar);
改進:釋放線程,但依然同步阻塞。
BinaryOp op = Add;
op.BeginInvoke(3, 2, ar => {
var result = ((BinaryOp)((AsyncResult)ar).AsyncDelegate).EndInvoke(ar);
Console.WriteLine($"Callback: {result}, State: {ar.AsyncState}");
}, "UserData");
Console.WriteLine("Main thread free to do other work...");
Console.ReadLine();
關鍵點:
AsyncResult.AsyncDelegate 取得原委託實例AsyncState 傳遞用戶數據BinaryOp op = Add;
op += Sub;
IAsyncResult ar = op.BeginInvoke(10, 5, null, null);
// 實際上會為鏈中每個委託分配獨立線程並行執行
// EndInvoke 僅返回最後一個完成的委託的結果
// 若需所有結果,必須拆分為單獨委託調用
EndInvoke 拋出,需在回調中捕獲.NET 4.0+ Task 統一了異步模型,C# 5.0 async/await 恢復同步寫法:
// 將 APM 封裝為 Task
public static Task AddAsync(int x, int y) {
var tcs = new TaskCompletionSource();
BinaryOp op = Add;
op.BeginInvoke(x, y, ar => {
try {
tcs.SetResult(op.EndInvoke(ar));
} catch (Exception ex) {
tcs.SetException(ex);
}
}, null);
return tcs.Task;
}
// 使用
public static async Task Demo() {
int sum = await AddAsync(3, 2); // 非阻塞等待
Console.WriteLine(sum);
// 並行
var tasks = new[] { AddAsync(1,2), AddAsync(3,4), AddAsync(5,6) };
int[] results = await Task.WhenAll(tasks);
// 超時取消
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2));
try {
await AddAsync(100, 200).WaitAsync(cts.Token);
} catch (OperationCanceledException) {
Console.WriteLine("Timed out");
}
}
| 維度 | Delegate APM | Task + async/await |
|---|---|---|
| 可讀性 | 回調地獄 | 同步風格,線性流程 |
| 異常處理 | 分散在 EndInvoke | try/catch 自然捕獲 |
| 組合 | 手動協調 | WhenAll/WhenAny/ContinueWith |
| 取消 | 無標準 | CancellationToken 內建 |
| 進度報告 | 自行實現 | IProgress<T> 標準化 |
| 調試 | 堆棧破碎 | 保留邏輯堆棟 |
| 性能 | 較輕量 | 狀態機開銷極小 |
Task.Factory.FromAsync 或 TaskCompletionSource 封裝BeginInvoke 在 .NET Core / .NET 5+ 上不支持 (跨平台限制),必須遷移委託異步是 C# 異步編程的「遠古遺跡」,理解它能讓你更珍惜 Task/async-await 的優雅設計。現代 .NET 開發者應將 APM 視為歷史包袱,全面擁抱統一異步模型——除非你在維護 .NET Framework 2.0 時代的遺產系統。