CodeGym /課程 /C# SELF /結合 lambda 表達式 和 委派(delegate)

結合 lambda 表達式 和 委派(delegate)

C# SELF
等級 50 , 課堂 4
開放

1. 介紹

Delegate(委派) — 是一種描述函式「簽名」的型別:它接受哪些參數、回傳什麼。就像函式的護照,或是程式碼的夜店通行證:簽名對上就進,對不上就別想進來。

範例:

public delegate int Operation(int a, int b);

這個 delegate 表示一個接受兩個 int 並回傳 int 的函式。下面是「沒有 lambda 時代」的寫法(新鮮但不太方便):

Operation add = delegate (int x, int y)
{
    return x + y;
};

有了 lambda,程式碼就簡潔多了:

Operation add = (x, y) => x + y;

有趣的事實: 在 C# 裡,lambda 表達式其實就是創建匿名函式的一種語法,編譯時會自動產生對應類型的 delegate。這就是為什麼它們看起來幾乎沒什麼區別!

內建的通用委派: Func, Action, Predicate

為了不用為每種情況都自訂 delegate,C# 提供了幾個「通用」的類型:

  • Func<T1, T2, ..., TResult> — 通用委派,接受參數 (T1, T2, ...) 並回傳 TResult
  • Action<T1, T2, ...> — 幾乎一樣,但不回傳值(void)。
  • Predicate<T> — 接受一個參數並回傳 bool(是所有「過濾器」的好朋友)。
委派 回傳型別 語法範例
Func<int, int>
int
Func<int, int> f = x => x + 1;
Action<string>
void
Action<string> a = s => ...;
Predicate<double>
bool
Predicate<double> p = d => ...;

範例:

Func<int, int, int> add = (a, b) => a + b;
Action<string> show = s => Console.WriteLine(s);
Predicate<int> isEven = n => n % 2 == 0;

2. lambda 表達式「包裝在」delegate 裡

把 lambda 當作有 delegate 參數的方法的引數

任何接收 delegate 的方法,都可以接受一個簽名相符的 lambda。

public static void CalculateAndShow(int a, int b, Func<int, int, int> operation)
{
    int result = operation(a, b);
    Console.WriteLine($"結果: {result}");
}

// 呼叫時傳入 lambda
CalculateAndShow(5, 7, (x, y) => x * y); // 結果: 35

使用 Action 的範例

void ProcessString(string message, Action<string> processor)
{
    processor(message);
}

// 使用 lambda
ProcessString("嗨,世界!", s => Console.WriteLine(s.ToUpper()));

使用 Predicate 的範例

List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6 };
List<int> evenNumbers = numbers.FindAll(n => n % 2 == 0);

就會覺得:為什麼還要自己寫一堆 delegate,當 Microsoft 已經給了輪子可以直接用?

delegate 和 lambda 怎麼一起工作

flowchart TD
    A[Lambda 表達式] -->|建立| B((Delegate))
    B -->|作為參數傳遞| C[方法]
    B -->|儲存| D[屬性/欄位/集合]
    C -->|呼叫| E[Lambda 的程式碼]

3. 在自己的方法中把 lambda 當作 delegate 傳入

如果你自己的方法使用 delegate,lambda 會讓 API 用起來超級彈性。

範例:我們的小型計算機

// 定義一個處理運算的函式
static int PerformOperation(int a, int b, Func<int, int, int> operation)
{
    return operation(a, b);
}

// 使用不同的運算(用 lambda)
int sum = PerformOperation(3, 4, (x, y) => x + y);
int multiply = PerformOperation(3, 4, (x, y) => x * y);
int max = PerformOperation(3, 4, (x, y) => x > y ? x : y);

Console.WriteLine(sum);      // 7
Console.WriteLine(multiply); // 12
Console.WriteLine(max);      // 4

看到沒?用一個方法就能靈活切換邏輯,省掉三個獨立的方法。

4. lambda + delegate 在 LINQ(和其他地方)

在 LINQ 裡,方法通常期待一個「小型邏輯函式」,也就是 delegate。幾乎所有常用的 LINQ 方法都接受這類型:

  • Func<T, bool> 用在過濾(Where
  • Func<T, TResult> 用在轉換(Select
  • Func<T, TKey> 用在排序(OrderBy
List<string> names = new List<string> { "安娜", "奧列格", "維克多", "亞娜" };

// 取得長度超過 3 的名字
var longNames = names.Where(name => name.Length > 3);

// 把名字轉成大寫
var upperNames = names.Select(name => name.ToUpper());

LINQ 可說是「lambda 的強化版」,凡是可以用 delegate 的地方都可以用 lambda!

5. 組合:儲存和傳遞 delegate-lambda

函式清單

lambda 不只是臨時匿名方法,你可以把一組 lambda 存在集合裡,然後動態使用它們。

List<Func<int, int, int>> operations = new List<Func<int, int, int>>
{
    (x, y) => x + y,
    (x, y) => x - y,
    (x, y) => x * y,
    (x, y) => x / y
};

foreach (var op in operations)
{
    Console.WriteLine(op(10, 2));
}

這招常見於測試、處理器和 plugin 架構中。

delegate 字典

如果你喜歡 plug-and-play 的架構,可以這樣做:

var mathFuncs = new Dictionary<string, Func<int, int, int>>
{
    { "plus", (x, y) => x + y },
    { "minus", (x, y) => x - y },
    { "pow", (x, y) => (int)Math.Pow(x, y) }
};

string command = "pow"; // 模擬使用者輸入

if (mathFuncs.TryGetValue(command, out var operation))
{
    Console.WriteLine(operation(2, 5)); // 32
}
else
{
    Console.WriteLine("找不到指令");
}

6. 有用的細節

lambda-delegate:從方法回傳 delegate

你可以從方法回傳 delegate(也就是 lambda)——幾乎就是函式工廠了!

Func<int, int> GetMultiplier(int factor)
{
    // 回傳一個會把數字乘上 factor 的 lambda delegate
    return x => x * factor;
}

var triple = GetMultiplier(3);
Console.WriteLine(triple(5)); // 15

var quadruple = GetMultiplier(4);
Console.WriteLine(quadruple(5)); // 20

這就是 closure(閉包)從玩具變成實用工具的例子!

函式合成:Combine、Delegate.Combine 與多重 delegate

有時候你希望一次呼叫能執行好幾個函式。例如 GUI 的事件處理:按按鈕時好幾個 handler 都要反應。

Action 和其他 delegate 支援把多個函式「串接」起來:

Action<string> pipeline = s => Console.WriteLine("步驟 1: " + s);
pipeline += s => Console.WriteLine("步驟 2: " + s.ToUpper());
pipeline += s => Console.WriteLine("步驟 3: " + s.Length);

pipeline("測試");
// 輸出:
// 步驟 1: 測試
// 步驟 2: 測試(大寫)
// 步驟 3: 2

這對於「鏈式」處理或事件分發很方便。

重點: 如果是有回傳值的函式,多重 delegate 只會保留 最後一個 呼叫的結果(也就是「尾值」)。而對於 Action,因為是 void,每個 handler 都會執行。

把 lambda delegate 當作 callback 參數使用

典型情境是「做完再叫我」的 callback 模式。

void DownloadFile(string url, Action<string> onFinish)
{
    // 模擬下載檔案...
    System.Threading.Thread.Sleep(500); // (模擬長時間工作,請勿在真實 GUI 應用中使用)
    onFinish($"下載 '{url}' 完成!");
}

DownloadFile("http://example.com", msg => Console.WriteLine(msg));

在真正的應用(像是 API、資料庫、檔案操作)裡,這是常見的模式。

高階函式:以 predicate 過濾的範例

lambda 和 delegate 讓你可以寫接受其他函式作為參數的通用函式,這就是高階函式的基礎。

List<int> Filter(List<int> source, Predicate<int> condition)
{
    List<int> result = new List<int>();
    foreach (var item in source)
        if (condition(item)) result.Add(item);
    return result;
}

// 用不同的 lambda:
var onlyPositive = Filter(new List<int> { -2, 0, 2, 7 }, x => x > 0);
var onlyEven = Filter(new List<int> { -2, 0, 2, 7 }, x => x % 2 == 0);

你甚至可以做「lambda 工廠」:

Func<int, Predicate<int>> GetRangeChecker(int min, int max) =>
    x => x >= min && x <= max;

Predicate<int> inRange = GetRangeChecker(1, 10);

var filtered = Filter(new List<int> { 0, 5, 10, 15 }, inRange); // 5, 10

7. 常見錯誤與組合時的注意事項

如果參數簽名不匹配,編譯器會抱怨(CS1660: "Cannot convert lambda expression")。

lambda 可能會意外「記住」後來會改變的變數(closure),所以對捕獲變數要小心。

多重 delegate 有回傳值時,只會取得最後一個處理器的回傳值,這對新手常常是驚喜(不是好驚喜)。

在需要 Expression<Func<int,bool>> 的地方,不能用 statement-lambda —— 必須是 expression-lambda(也就是可以轉成 expression tree 的那種寫法)。

2
任務
C# SELF, 等級 50, 課堂 4
上鎖
使用 Func 和 Action
使用 Func 和 Action
1
問卷/小測驗
委派 - 給 lambda 表達式用的型別,等級 50,課堂 4
未開放
委派 - 給 lambda 表達式用的型別
lambda 表達式跟委派的關係
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION