1. 引言
知道吗?lambda 表达式本身就像一份没有锅的菜谱。它描述了要做什么,但需要一个“容器”才能在代码里存在。在 C# 里,这个容器就是现成的通用委托类型:Func, Action 和 Predicate。
把它们想象成给你的 lambda 的模具——拿合适的模具,把你的逻辑倒进去就行了。没必要自己造轮子或定义自有委托类型,大多数情况下标准的已经够用了。
一点历史
一开始,如果你想把函数作为参数传递,就得声明自己的委托类型。那很冗长、很繁琐,看起来像这样:
delegate int Calculate(int x, int y);
Calculate adder = (a, b) => a + b;
当 C# 引入了 Func, Action 和 Predicate 后,90% 的场景就不用手动声明委托了。现在看起来简单多了、更通用:
Func<int, int, int> adder = (a, b) => a + b;
2. Func<T, TResult> — 有返回值的函数
语法和用途
Func 是一个泛型委托,它可以接受从 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());
可视化示意图
| 签名 | 示例 | 说明 |
|---|---|---|
|
|
接受 int,返回 int |
|
|
两个 int,返回 int |
|
|
不接受参数,返回字符串 |
在你的应用里长什么样
举个例子,在我们的小应用(假设的“用户手册”)里,有一个数字列表——想对它做不同的处理。比如求平方或跟固定数字相加:
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
可视化示意图
| 签名 | 示例 | 说明 |
|---|---|---|
|
|
无参数,无返回 |
|
|
一个参数 |
|
|
多个参数 |
在我们的应用中
给“用户手册”加一个打印所有名字的方法:
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 —— 官方的“我们需要布尔检查”包装器,专门用来写这种 lambda。
示例
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 示例 | 使用场景 |
|---|---|---|---|
|
T → U | |
Select, 各种转换 |
|
T → void | |
ForEach, 执行动作的方法 |
|
T → bool | |
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; 这种写法不能工作,因为表达式返回了值,但 Action 不允许返回值。
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); // 可以通过构造器转换
GO TO FULL VERSION