「Don't call me, I'll call you」——好萊塢法則的程式碼體現。當你呼叫函式 A,並將函式 B 作為參數傳遞給 A,讓 A 在適當時機「回頭呼叫」B,這個 B 就是回調函數。它將「控制權」反轉給呼叫者,實現邏輯解耦。
C++ 使用函式指標實現回調;C# 則以委派 為核心機制。委派是型別安全的物件導向函式指標,可封裝靜態方法、實例方法、Lambda 運算式,並支援多播(串聯多個目標方法)。
// 定義委派型別:雙參數運算策略
public delegate double BinaryOp(double x, double y);
public static double Multiply(double x, double y) => x * y;
public static double Divide(double x, double y) => x / y;
// 執行引擎:不關心具體運算,只負責流程控制
public static void Execute(BinaryOp operation)
{
double result = operation(2.2, 3.3); // 回調點
Console.WriteLine($"結果:{result}");
}
// 使用端:根據條件動態決定策略
public static void Main()
{
BinaryOp op = Console.ReadKey().KeyChar == 'M' ? Multiply : Divide;
Execute(op); // 輸出:7.26 或 0.666...
}
對應場景: 排序比較器、驗證規則、壓縮演算法選擇等「策略模式」實作。
這是 .NET 事件模型的基石。event 關鍵字在委派外層加上「只能 += / -=,不能直接呼叫/指派」的封裝,語意更清晰。
public class FileDownloader
{
// 事件:下載進度變更
public event Action ProgressChanged;
public async Task DownloadAsync(string url)
{
for (int i = 0; i <= 100; i += 10)
{
await Task.Delay(100); // 模擬下載
ProgressChanged?.Invoke(i); // 安全觸發回調
}
}
}
// 訂閱端
var downloader = new FileDownloader();
downloader.ProgressChanged += p => Console.WriteLine($"進度:{p}%");
await downloader.DownloadAsync("https://example.com/file.zip");
雖然現代 C# 主要用 await,但 Task.ContinueWith 底層仍是委派回調機制。
Task compute = Task.Run(() => HeavyCalculation());
compute.ContinueWith(t =>
{
Console.WriteLine($"計算完成,結果:{t.Result}");
}, TaskScheduler.FromCurrentSynchronizationContext()); // 確保回 UI 執行緒
重構原始碼範例,展示委派如何解決「背景執行緒安全更新 UI」問題。
public partial class CallbackDemo : Form
{
// 1. 定義 UI 更新委派(型別安全)
private delegate void UpdateTextBoxDelegate(char ch);
// 2. 宣告兩個獨立的回調目標方法
private void UpdateTextBox1(char ch) => textBox1.AppendText(ch + "\r");
private void UpdateTextBox2(char ch) => textBox2.AppendText(ch + "\r");
// 3. 背景工作方法:接收委派參數,執行時回調
private void BackgroundWork(string text, UpdateTextBoxDelegate callback)
{
foreach (char c in text)
{
// 關鍵:透過 Control.Invoke 將委派封送至 UI 執行緒執行
this.Invoke(callback, c);
Thread.Sleep(100); // 模擬工作
}
}
// 4. 按鈕點擊:啟動兩條獨立執行緒,各自綁定不同回調
private void btnStart_Click(object sender, EventArgs e)
{
if (chkBox1.Checked)
{
new Thread(() => BackgroundWork(txtInput.Text, UpdateTextBox1)).Start();
}
if (chkBox2.Checked)
{
new Thread(() => BackgroundWork(txtInput.Text, UpdateTextBox2)).Start();
}
}
}
.NET Framework 3.5+ 內建 Func<T, TResult>、Action<T> 等泛型委派,大多數情況下無需自行 delegate 宣告:
// 等價於 delegate double BinaryOp(double, double)
Func op = (x, y) => x * y;
// 等價於 delegate void UpdateTextBox(char)
Action update = c => textBox.AppendText(c.ToString());
| 陷阱 | 後果 | 解決方案 |
|---|---|---|
| 迴圈中捕捉迴圈變數 | 所有回調共用同一變數,最終值相同 | 在迴圈內建立區域變數副本 |
| 非同步回調未捕捉例外 | 例外被吞噬,難以除錯 | 在回調內部 try-catch 或使用 Task 機制 |
| 長生命週期物件持有短生命週期委派 | 記憶體洩漏(無法 GC) | 使用弱事件模式或明確取消訂閱 |
| UI 執行緒直接呼叫耗時回調 | 介面凍結 | 回調務必快速返回,耗時工作放背景執行緒 |
委派是 C# 實現「控制反轉」「依賴注入」「事件驅動」的語言級基石。掌握委派回調模式,不僅能寫出可擴充、可測試的程式碼,更能深入理解 LINQ、ASP.NET Core Middleware、Entity Framework 事件攔截等現代框架的核心運作機制。