委派是 C# 實現「函式即資料」的核心機制,也是事件、Lambda、LINQ、非同步程式設計的基石。本文系統整理委派宣告、實例化、多播、匿名方法、Lambda、泛型委派 (Func/Action)、非同步模式、以及常見設計模式應用。
// 宣告委派型別:回傳 double,接受兩個 double 參數
delegate double BinaryOp(double x, double y);
static double Add(double x, double y) => x + y;
static double Sub(double x, double y) => x - y;
void Demo()
{
// 實例化:方法群組轉換
BinaryOp op = Add;
double result = op(3, 5); // 8
// 重新指派
op = Sub;
result = op(10, 4); // 6
}
委派可串聯多個目標方法,呼叫時依序執行,回傳值為最後一個方法的結果。
BinaryOp multi = Add;
multi += Sub; // 串聯
multi += (x, y) => x * y; // Lambda
double r = multi(10, 2); // 依序執行 Add(12)、Sub(8)、Mul(20) → 回傳 20
// 手動遍歷調用清單(取得所有回傳值)
foreach (BinaryOp d in multi.GetInvocationList())
{
Console.WriteLine(d(10, 2)); // 12, 8, 20
}
.NET 內建常用委派,免去自訂宣告:
| 委派 | 參數 | 回傳 | 用途 |
|---|---|---|---|
Action | 0 | void | 無參無回傳動作 |
Action<T> | 1 | void | 單參動作 |
Action<T1,T2> | 2 | void | 雙參動作 |
Func<TResult> | 0 | TResult | 無參函式 |
Func<T, TResult> | 1 | TResult | 單參函式 |
Func<T1,T2,TResult> | 2 | TResult | 雙參函式 |
Predicate<T> | 1 | bool | 條件判斷 |
Func op = (x, y) => x + y;
Action log = Console.WriteLine;
Predicate isEven = n => n % 2 == 0;
// C# 2.0 匿名方法
BinaryOp op1 = delegate(double x, double y) { return x + y; };
// C# 3.0 Lambda 運算式
BinaryOp op2 = (x, y) => x + y; // 運算式主體
BinaryOp op3 = (x, y) => { return x + y; }; // 區塊主體
// 參數型別推論、單一參數可省略括號
Func square = x => x * x;
Action greet = () => Console.WriteLine("Hi");
void ClosureDemo()
{
int factor = 10;
Func multiply = n => n * factor; // 捕獲 factor
factor = 20;
Console.WriteLine(multiply(5)); // 100 (捕獲的是變數本身,非複製值)
}
陷阱:迴圈中捕獲迴圈變數會導致所有委派共用同一變數,需在迴圈內建立區域副本。
注意:現代 .NET 建議使用 Task.Run / async-await,此處僅作歷史架構維護參考。
delegate int LongRunningOp(int x);
LongRunningOp op = n => { Thread.Sleep(2000); return n * 2; };
// 啟動非同步
IAsyncResult ar = op.BeginInvoke(5, null, null);
// 1. 同步等待 (阻塞)
int result = op.EndInvoke(ar);
// 2. 輪詢
while (!ar.IsCompleted) Thread.Sleep(100);
result = op.EndInvoke(ar);
// 3. 回調
op.BeginInvoke(5, ar2 => {
int r = op.EndInvoke(ar2);
Console.WriteLine($"完成:{r}");
}, null);
interface IDiscountStrategy { decimal Calc(decimal price); }
class VipDiscount : IDiscountStrategy { public decimal Calc(decimal p) => p * 0.8m; }
class NormalDiscount : IDiscountStrategy { public decimal Calc(decimal p) => p * 0.95m; }
// 使用委派替代介面(輕量級策略)
Func vip = p => p * 0.8m;
Func normal = p => p * 0.95m;
decimal price = 100;
Console.WriteLine(vip(price)); // 80
class Publisher
{
public event Action OnMessage; // 事件 = 封裝的多播委派
public void Notify(string msg) => OnMessage?.Invoke(msg);
}
var pub = new Publisher();
pub.OnMessage += msg => Console.WriteLine($"Sub1: {msg}");
pub.OnMessage += msg => File.AppendAllText("log.txt", msg + Environment.NewLine);
pub.Notify("Hello");
using Middleware = Func, Task>;
var pipeline = new List
{
async (ctx, next) => { Console.WriteLine("Before 1"); await next(); Console.WriteLine("After 1"); },
async (ctx, next) => { Console.WriteLine("Before 2"); await next(); Console.WriteLine("After 2"); }
};
Func end = () => Task.CompletedTask;
foreach (var mw in pipeline.Reverse())
{
var next = end;
end = () => mw(new HttpContext(), next);
}
await end();
| 陷阱 | 說明 | 解法 |
|---|---|---|
| 迴圈閉包 | foreach/for 變數被共用 | 迴圈內建立區域變數 var copy = item; |
| 事件記憶體洩漏 | 訂閱者未取消訂閱,導致無法 GC | 實作 IDisposable 取消訂閱,或使用弱事件模式 |
| 多執行緒調用清單變動 | 遍歷時其他執行緒 +=/-= | 先 var list = del.GetInvocationList(); 再遍歷副本 |
| 例外中斷多播鏈 | 前一個方法拋出例外,後續不執行 | 手動遍歷 GetInvocationList() 包裝 try-catch |
委派是 C# 函數式程式設計的入口:
Func/Action/Predicate 涵蓋 99% 場景,優先使用。Begin/EndInvoke,全面擁抱 Task/async-await。熟練運用委派,能讓程式碼更靈活、可測試、符合開放封閉原則,是邁向高階 C# 開發者的關鍵技能。