1. 介紹
到目前為止你已經看到:我們呼叫那些接收 lambda 的 LINQ 方法。但 C# 的方法怎麼知道可以傳入一個函式呢?
簡單說,delegate 就像函式的介面:我們描述簽名(參數型別和回傳值),任何方法(或 lambda!)只要符合這個簽名,就能被傳到期待該 delegate 的位置。
還記得你怎麼把字串當作參數傳遞嗎?處理“邏輯”也是差不多,只是參數的類型變成了 delegate。
委派(delegate):白話的基本理論
在 C# 裡,delegate 是描述「具有某種簽名的函式」的型別。
// 一個接受 int 並回傳 bool 的 delegate
public delegate bool IntPredicate(int x);
任何函式,只要簽名相容,都可以賦值給這種型別的變數:
bool IsEven(int n) => n % 2 == 0;
IntPredicate pred = IsEven;
現在 lambda 也可以:
IntPredicate pred = x => x % 2 == 0;
泛用 delegate:Func, Action, Predicate
- Func<T1, ..., TResult> — 函式,接受參數 T1, ... 並回傳 TResult。
- Action<T1, ...> — 函式,接受參數但不回傳值(void)。
- Predicate<T> — 函式,接受 T 並回傳 bool。
2. 在自己的方法中傳遞 lambda
假設我們在開發教學用的小程式 — 一個操作使用者清單的 console 專案。之前我們用 LINQ 來過濾集合,現在我們來寫一個自家的方法,接收一個 lambda 條件。
建立一個有 lambda 參數的方法
// 為範例定義 User 類別(放到我們的應用裡)
public class User
{
public string Name { get; set; }
public bool IsActive { get; set; }
}
// 接受使用者清單和 delegate 條件(lambda)的 方法
public static List<User> FilterUsers(List<User> users, Predicate<User> predicate)
{
var result = new List<User>();
foreach (var user in users)
{
if (predicate(user)) // 呼叫 lambda!
result.Add(user);
}
return result;
}
現在可以傳任何 lambda:
var users = new List<User>
{
new User { Name = "瓦西亞", IsActive = true },
new User { Name = "佩佳", IsActive = false },
new User { Name = "瑪莎", IsActive = true }
};
// 只篩選啟用的使用者
var activeUsers = FilterUsers(users, user => user.IsActive);
foreach (var user in activeUsers)
Console.WriteLine(user.Name); // 瓦西亞, 瑪莎
就這樣!我們把一段邏輯 — 小函式 — 作為普通參數傳入,因為方法 FilterUsers 等待的是 Predicate<User>,而我們給了它相容的 lambda。
使用 Func<T, TResult> 的變體
Predicate<T> 適合用在需要條件(回傳 bool)的場景。若是想對每個使用者「計算」些什麼,該用 Func<T, TResult>。
// 對每個元素套用函式並收集結果的 方法
public static List<TResult> MapUsers<TResult>(List<User> users, Func<User, TResult> selector)
{
var result = new List<TResult>();
foreach (var user in users)
{
result.Add(selector(user));
}
return result;
}
使用範例:
var names = MapUsers(users, user => user.Name.ToUpper());
foreach (var name in names)
Console.WriteLine(name); // 瓦西亞, 佩佳, 瑪莎
3. 有用的注意事項
不同的傳遞形式
你不只能傳 lambda,還可以傳一般的方法 — 重點是簽名要吻合。
// 一般方法
static bool NameHasS(User user) => user.Name.Contains("s");
// 傳一般方法:
var usersWithS = FilterUsers(users, NameHasS);
// 傳 lambda
var usersWithA = FilterUsers(users, u => u.Name.Contains("a"));
也可以用舊式匿名方法(不推薦):
var usersWithM = FilterUsers(users, delegate(User u) { return u.Name.Contains("m"); });
現代風格是用 lambda!
把 lambda 傳給 LINQ:實際發生了什麼
var result = users.Where(u => u.IsActive).ToList();
底層上 Where 接受的是 Func<User, bool>。這代表任何接受 Func<...> 的方法,都可以用同樣的方式呼叫!
如果想要兩個參數怎麼辦?
// 方法接受兩個 lambda 來做過濾
public static List<User> FilterUsersCustom(
List<User> users,
Func<User, bool> include,
Func<User, bool> exclude)
{
var result = new List<User>();
foreach (var user in users)
{
if (include(user) && !exclude(user))
result.Add(user);
}
return result;
}
使用範例:
var customFiltered = FilterUsersCustom(
users,
u => u.Name.StartsWith("V"),
u => u.IsActive == false
);
// 只會取名字以 "V" 開頭且為啟用的使用者
情境:過濾器工廠
Console.WriteLine("請輸入名字的最小長度:");
int minLength = int.Parse(Console.ReadLine());
Predicate<User> lengthFilter = user => user.Name.Length >= minLength;
var filteredUsers = FilterUsers(users, lengthFilter);
// 相當互動又有趣!
4. 典型錯誤和注意點
有時候編譯器無法正確「推斷」lambda 的參數型別 — 特別是在有 overloads(多載)或方法需要特定參數數量/回傳型別的 delegate 時。這種情況可以顯式標註 lambda 的型別:
FilterUsers(users, (User u) => u.Name.Length > 3);
或是:
MapUsers(users, (User u) => u.Name.ToUpper());
錯誤:lambda 與簽名不匹配
FilterUsers(users, user => Console.WriteLine(user.Name)); // 錯誤! 預期 bool,得到 void
因為預期的是回傳 bool 的函式,而這個 lambda 回傳的是 void(實際上沒有明確回傳)。注意回傳型別很重要!
錯誤:濫用 lambda
如果你的 lambda 寫到 10 行,那就把它抽出成一個獨立的方法。這樣比較易讀,也比較好除錯。
GO TO FULL VERSION