CodeGym /課程 /C# SELF /Lambda 表達式 與 Func, ...

Lambda 表達式 與 Func, Action, Predicate

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

1. 介紹

你知道嗎?Lambda 表達式本身就像一個沒鍋子的食譜。它描述了要做什麼,但需要一個「容器」才能在程式碼裡存在。C# 有現成的通用委派類型可以當這個容器:Func、Action 和 Predicate。

把它們想像成給你 Lambda 的模具——挑對的模子把你的邏輯倒進去就行。別再自幹自造委派類型了,90% 情況下已有標準解。

小小歷史

一開始,當需要把函式作為參數傳遞時,你得自己聲明委派類型。那既耗時又麻煩,像這樣:

delegate int Calculate(int x, int y);

Calculate adder = (a, b) => a + b;

當 C# 引入了 Func, Action 和 Predicate,你就可以在大多數情況下忘掉手動定義委派了。現在看起來簡潔又通用:

Func<int, int, int> adder = (a, b) => a + b;

2. Func<T, TResult> — 有回傳值的函數

語法與用途

Func 是一個泛型委派(generic delegate),它可以接受從 0 到 16 個參數,並且會回傳一個值。

Func<int, int, int> sum = (x, y) => x + y;

Func<型別1, 型別2, ..., 型別N, TResult> — 除了最後一個以外的所有型別參數都是輸入參數,最後一個是回傳型別。

範例

1. 兩數相加

Func<int, int, int> sum = (x, y) => x + y;
Console.WriteLine(sum(3, 5)); // 8

2. 平方

Func<int, int> square = x => x * x;
Console.WriteLine(square(4)); // 16

3. 無參數

Func<string> greet = () => "嗨,Lambda!";
Console.WriteLine(greet());

視覺示意

簽名 範例 說明
Func<int, int>
x => x * 2
接受 int,返回 int
Func<int, int, int>
(a, b) => a + b
兩個 int,返回 int
Func<string>
() => "hi"
不接受任何參數,返回字串

在你的應用中長什麼樣

假設在我們的小應用(假想的「使用者名錄」)裡,有一個整數清單 — 想對它做不同處理。例如,把每個數平方或跟一個固定數相加:

List<int> numbers = new() { 1, 2, 3, 4, 5 };

Func<int, int> square = x => x * x;
var squares = numbers.Select(square);
Console.WriteLine(string.Join(", ", squares)); // 1, 4, 9, 16, 25

3. Action<T> — 沒回傳值的動作

Action 是用來表示那些做事情(比如印到螢幕),但不回傳任何值的方法的通用委派。

它可以接受從 0 到 16 個參數,但一定會回傳 void。

範例

1. 印到螢幕

Action<string> print = text => Console.WriteLine("資料: " + text);
print("嗨,世界!");

2. 無參數的動作

Action greet = () => Console.WriteLine("歡迎!");
greet();

3. 多參數的動作

Action<int, int> showSum = (a, b) => Console.WriteLine($"總和: {a + b}");
showSum(2, 3); // 總和: 5

視覺示意

簽名 範例 說明
Action
() => ...
無參數、無回傳
Action<int>
x => ...
一個參數
Action<int, string>
(x, s) => ...
多個參數

在我們的應用中

在使用者名錄中加入一個列印所有名字的方法:

List<string> names = new() { "安娜", "鮑里斯", "維卡" };
Action<string> printName = name => Console.WriteLine("使用者: " + name);

names.ForEach(printName);
// 或者這樣: names.ForEach(name => Console.WriteLine(name));

4. Predicate<T> — 是或不是?

當你需要一個函數,對某個參數只回傳 true 或 false,就用 Predicate<T>。它就是一個接受一個參數並回傳 bool 的委派。

Predicate — 官方上說就是「我們需要一個布林檢查」的包裝器。

範例

1. 檢查是否大於 5

Predicate<int> isGreaterThanFive = x => x > 5;

Console.WriteLine(isGreaterThanFive(3)); // false
Console.WriteLine(isGreaterThanFive(7)); // true

2. 與方法 List<T>.Find 的搭配

List<int> values = new() { 2, 4, 7, 10 };
int found = values.Find(isGreaterThanFive); // 使用 Predicate<int>
Console.WriteLine(found); // 7

3. 都是成年人嗎?

List<int> ages = new() { 12, 19, 34 };

bool allAdults = ages.TrueForAll(age => age >= 18);
// TrueForAll 接受 Predicate<int>

和 Func<T, bool> 的差別

實際上,它們在很多情況下可以互換。就連 Microsoft 文件也說:"Predicate<T> 只是針對特定 API 的 Func<T, bool>"。但是標準函式庫有時會明確要求 Predicate。

5. Lambda 怎麼「套」進 Func, Action, Predicate

當你寫一個 lambda,C# 會分析:「喔,這個形式剛好符合要求的委派 —— 可以直接代入!」

Func<int, int> f1 = x => x * 2;
Action<string> a1 = text => Console.WriteLine(text);
Predicate<int> p1 = x => x < 10;

到處都是 lambda!但底層是三個不同簽名的委派。

在「真實」程式碼的應用

List<User> users = new() {
    new User("安娜", 24),
    new User("鮑里斯", 17),
    new User("維卡", 31),
};

// 函數:只回傳成年使用者 (Predicate<User>)
List<User> adults = users.FindAll(user => user.Age >= 18);
Console.WriteLine("成年名單: " + string.Join(", ", adults.Select(u => u.Name)));

如果想用 Action<User> 印出所有使用者名字:

users.ForEach(user => Console.WriteLine(user.Name));

取得他們的名字(Func<User, string>):

IEnumerable<string> names = users.Select(user => user.Name);

對照表(方便記憶)

委派 簽名 Lambda 範例 哪裡用到
Func<T, U>
T → U
user => user.Name
Select、各種轉換
Action<T>
T → void
user => Console.WriteLine(...)
ForEach、執行動作的方法
Predicate<T>
T → bool
user => user.Age > 18
Find, Exists, 過濾

6. 內建應用範例:一步步來

我們來擴充小應用「使用者名錄」。假設有一個 User 類別:

public class User
{
    public string Name { get; }
    public int Age { get; }
    public bool IsActive { get; set; }

    public User(string name, int age)
    {
        Name = name;
        Age = age;
        IsActive = true;
    }
}

1. Func<User, bool> — 檢查使用者是否成年

Func<User, bool> isAdult = user => user.Age >= 18;

在 LINQ 中使用:

var adults = users.Where(isAdult);

2. Predicate<User> — 找到一個啟用中的使用者

Predicate<User> isActive = user => user.IsActive;
User found = users.Find(isActive);

3. Action<User> — 將使用者設為非啟用

Action<User> deactivate = user => user.IsActive = false;
users.ForEach(deactivate);

4. Func<User, string> — 取得簡短描述

Func<User, string> describe = user => $"{user.Name} ({user.Age})";
var descriptions = users.Select(describe);

這些 lambda 本質上就是「以資料形式存在的程式碼」,可以傳給方法、存在變數裡、或組合起來使用。

7. 不那麼明顯的細節與常見錯誤

1. Lambda 必須符合委派簽名。 如果簽名不符,會編譯錯誤。

Func<int, string> wrong = x => x * 2; // 錯誤: 預期 string,但得到 int
// 正確寫法:
Func<int, string> right = x => (x * 2).ToString();

2. 別忘了 void 跟 return 的差別。 Action 不會回傳值——像是 Action<int> a = x => x * x; 這種會回傳值的寫法會失敗,因為不該有回傳值。

3. Predicate<T> 與 Func<T, bool> 常可互換,但不是每次都行。 有時集合的方法會明確要求 Predicate<T>,有時要 Func<T, bool>。直接賦值不一定能通過編譯,可能要顯式包裝。

Predicate<int> pred = x => x > 0;
Func<int, bool> func = pred; // 錯誤
// 但可以:
Func<int, bool> func2 = x => x > 0;
Predicate<int> pred2 = new Predicate<int>(func2); // 可以用建構子轉換
2
任務
C# SELF, 等級 50, 課堂 1
上鎖
使用 `Action` 處理字串清單
使用 `Action` 處理字串清單
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION