1. 介紹
想像你有一台咖啡機。通常它只負責煮咖啡,不會去做別的事,但有時主管會說:「你能不能在煮完咖啡後大喊『完成了!』?」— 這時就需要一個設定。我們不會重寫咖啡機本身,只是建立一個函式,讓它在結尾時呼叫。
在 C# 裡,這個角色就是 delegate — 它讓你可以把程式邏輯(方法、lambda 或匿名方法)傳進另一個方法,讓那個方法在需要的時候呼叫它。簡單講,delegate 是一種型別,可以儲存指向符合特定簽名的方法的引用。
Delegate 的定義
在 C# 中,delegate 是用關鍵字 delegate 來定義。範例:
// Delegate,會參考那些接受 int 並回傳 bool 的方法
public delegate bool PredicateInt(int x);
現在類型為 PredicateInt 的變數可以引用任何接受一個 int 並回傳 bool 的方法(或 lambda)。
delegate 有什麼用?
- 把邏輯以參數的形式傳遞(例如用在排序、過濾、處理事件)
- 訂閱事件(這部分後面會講)
- 實作回呼(callback)
- 建立彈性 API,讓呼叫者決定部分行為
簡單的視覺示意
| Delegate 類型 | 簽名 | 呼叫範例 |
|---|---|---|
|
|
|
|
|
|
|
|
|
2. lambda 怎麼變成 delegate
語法
當你寫一個 lambda 表達式,例如 x => x > 5,本質上你是在建立一個 delegate 物件。lambda 不會「孤立存在」:它一定需要一個型別(要有人知道參數和回傳值長什麼樣)。所以在 C# 裡,lambda 表達式總是會被隱含地(或明確地)轉換成某個 delegate。
範例 1:把 delegate 綁到方法
// 明確定義 delegate 類型
public delegate bool MyPredicate(int number);
class Program
{
static void Main()
{
// 把 lambda 指派給 MyPredicate 類型的變數
MyPredicate isEven = x => x % 2 == 0;
Console.WriteLine(isEven(4)); // true
Console.WriteLine(isEven(7)); // false
}
}
範例 2:使用內建的 delegate
C# 內建了一組泛型 delegate:Action、Func<>、Predicate<>。幾乎在你寫 lambda 的地方都會看到它們。
// 使用 Func<int, bool>
Func<int, bool> isPositive = number => number > 0;
Console.WriteLine(isPositive(-5)); // false
3. 內建 delegate:Func, Action, Predicate
Func<...>
用於那些會接受參數並回傳值的方法。
簽名:
— 最後一個型別是回傳值,之前的都是參數型別,例如:
Func<int, string> — 接受 int,回傳 string
Func<int, string> intToString = number => "數字: " + number;
Console.WriteLine(intToString(7)); // "數字: 7"
Action<...>
用在只要執行某事但不需回傳(void)的情況。
Action<string> printHello = name => Console.WriteLine("嗨, " + name + "!");
printHello("瓦西里"); // "嗨, 瓦西里!"
Predicate<T>
本質上是 Func<T, bool> 的簡寫。用在需要對物件做布林檢查(true/false)的場景。
Predicate<int> isOdd = x => x % 2 != 0;
Console.WriteLine(isOdd(3)); // true
快速參考
| Delegate | 簽名 | 用途 |
|---|---|---|
|
|
轉換、投影 |
|
|
副作用、輸出 |
|
|
過濾、搜尋 |
要為 lambda 選哪個 delegate?
- 如果預期有回傳值,就用 Func<...>
- 如果方法不回傳任何東西(void),用 Action<...>
- 如果需要做條件檢查,使用 Predicate<T>
範例:過濾清單
List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6 };
// FindAll 期望的是 Predicate<int>
List<int> evenNumbers = numbers.FindAll(x => x % 2 == 0);
Console.WriteLine(string.Join(", ", evenNumbers)); // 2, 4, 6
4. 一些實用細節
lambda 與集合方法:底層發生了什麼?
當你呼叫集合方法並傳入 lambda,例如:
var adults = users.Where(u => u.Age >= 18);
方法 Where 期望的參數型別是 Func<T, bool>。也就是說,你的 lambda u => u.Age >= 18 會被編譯器轉成該型別的 delegate 物件。
流程圖:運作方式
你的 lambda --> 編譯器 C# --> delegate 物件 (Func<T, bool>)
(u => u.Age >= 18) [型別已知] (準備好可在 Where() 中被呼叫)
再說型別:型別推導
通常編譯器會自動推導出 delegate 的型別,只要上下文清楚。例如對於方法 List<T>.Find,它會期望 Predicate<T>,而編譯器會從方法簽名知道參數的型別。
List<string> words = new List<string> { "one", "two", "three" };
var result = words.Find(word => word.Length == 5); // Find 期望 Predicate<string>
Console.WriteLine(result); // "three"
如果上下文不明確,你就得幫編譯器一把:
// 明確指定型別
Func<int, bool> check = x => x > 10;
回傳 delegate:函數工廠
有時候方法會回傳 delegate,也就是建立「函數工廠」。這對於產生動態行為很方便。
// 回傳 delegate(lambda)的函數
Func<int, int> GetMultiplier(int factor)
{
return x => x * factor;
}
var times3 = GetMultiplier(3);
Console.WriteLine(times3(5)); // 15
這能工作的原因是 lambda (x => x * factor) 捕獲了外部的變數 factor(closure/閉包),並且被當作 Func<int, int> 型別的物件回傳。
5. 常見錯誤與誤解(delegate 與 lambda)
簽名不匹配
如果參數或回傳型別不一致,編譯器不會允許把 lambda 指派給該 delegate。
Func<int, int> f = x => "不能回傳字串!"; // 編譯錯誤
嘗試在沒有 delegate 的情況下使用 lambda 時的錯誤
你不能直接寫一個 lambda 然後嘗試呼叫它而不給它一個型別:
// 這樣不會成功 - 編譯器無法推導型別
// var myFunc = x => x * 2; // 錯誤 CS0815
// myFunc(10);
要讓它工作,你需要明確指定型別或提供上下文:
Func<int, int> myFunc = x => x * 2;
Console.WriteLine(myFunc(10)); // 20
混淆 Action、Func 和 Predicate
有時候會不小心選錯 delegate 類型,導致簽名不匹配。記住簡單規則:Func — 有回傳值時用,Action — 沒有回傳值(void)時用,Predicate<T> — 需要布林判斷(bool)時用。
GO TO FULL VERSION