1. 介绍
到目前为止你已经看到:我们调用过一些 LINQ 的方法,它们接受 lambda 作为参数。但 C# 方法怎么知道“可以传入一个函数”呢?
简单来说,委托(delegate)就像函数的接口:我们定义签名(参数类型和返回值),任何符合这个签名的方法(包括 lambda!)都可以传到期待该委托的地方。
还记得你怎么把字符串作为参数传吗?把“逻辑”当参数也是类似的,只不过参数类型是委托。
委托:通俗易懂的基础理论
在 C# 里,委托是描述“具有某种签名的函数”的类型。
// 一个接收 int 并返回 bool 的委托
public delegate bool IntPredicate(int x);
任何与签名兼容的函数都可以赋值给这个类型的变量:
bool IsEven(int n) => n % 2 == 0;
IntPredicate pred = IsEven;
现在 —— lambda 也可以:
IntPredicate pred = x => x % 2 == 0;
通用委托: Func, Action, Predicate
- Func<T1, ..., TResult> — 一个接收参数 T1, ... 并返回 TResult 的函数。
- Action<T1, ...> — 一个接收参数但不返回值(void)的函数。
- Predicate<T> — 一个接收 T 并返回 bool 的函数。
2. 把 lambda 传到自定义方法
假设我们在开发一个教学用的小项目 —— 控制台程序,操作一个用户列表。之前我们通过 LINQ 过滤集合,这次写一个自己的方法,接受一个 lambda 条件。
创建一个带 lambda 参数的方法
// 定义示例用的 User 类(加入到我们的应用里)
public class User
{
public string Name { get; set; }
public bool IsActive { get; set; }
}
// 接收列表和委托条件(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)的时候。如果想对每个用户做点“计算”,怎么办?
// 对每个元素应用函数并收集结果的方法
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 参数的类型 —— 特别是在重载复杂、或者方法需要特定返回类型/多个参数的委托时。此时可以显式写出 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