C# Lambda概述
2026-09-30
Lambda 是 C# 里书写匿名函数的语法。它没有名字,可以赋给委托,也可以直接当作参数传给其他方法。
什么是 Lambda
委托(delegate)保存一份可以稍后调用的方法。匿名方法是不单独起名的方法体。Lambda 是书写委托实例的简写,赋给委托后就是一个可调用的委托。
下面三行做的是同一件事:把“整数乘 2”交给 Func<int, int>。
Func<int, int> doubleValue = delegate (int x) { return x * 2; };
Func<int, int> doubleValue2 = (int x) => x * 2;
Func<int, int> doubleValue3 = x => x * 2;Lambda 的优点
- 短逻辑写在调用处,阅读时不用跳到另一个方法。
- 可以把一段行为当作参数,传给
Find、Sort、Where等方法。 - 配合 LINQ,筛选、投影和排序可以连写成一条链。
需要在多处复用、或要单步调试的逻辑,写成命名方法。捕获外部变量时,编译器会生成闭包对象,这次分配发生在堆上。
基本语法
表达式体只有一个表达式,这个表达式的值就是返回值:
Func<int, int, int> add = (x, y) => x + y;语句体用花括号包住多条语句。有返回值时写出 return:
Func<int, int> abs = x =>
{
if (x < 0)
{
return -x;
}
return x;
};参数有四种常见写法:
- 没有参数时写出空括号:
() => ... - 只有一个参数、且类型能推断时,可以省略括号:
x => x * 2 - 多个参数必须加括号:
(x, y) => x + y - 多条语句必须使用花括号;需要结果时显式
return
常用表达式
无参数
适合无参回调:
Action sayHi = () => Console.WriteLine("hi");
sayHi();单参数
类型能推断时,括号可以省略:
Func<int, int> twice = x => x * 2;
Console.WriteLine(twice(21));多参数
Func<int, int, int> sum = (x, y) => x + y;
Console.WriteLine(sum(1, 2));显式参数类型
类型推断失败,或希望签名更明确时,写出参数类型。只要有一个参数写了类型,其余参数也要写:
Func<int, string, string> format = (int x, string s) => $"{s}:{x}";
Console.WriteLine(format(3, "id"));语句 Lambda
方法体里可以声明局部变量,也可以提前返回:
Func<int, string> describe = n =>
{
if (n < 0)
{
return "负数";
}
int doubled = n * 2;
return $"非负,两倍是 {doubled}";
};丢弃参数
用不到的参数写成 _。从 C# 9 起,多个参数都可以丢掉:
Func<int, int, int> zero = (_, _) => 0;static Lambda
static 从 C# 9 起可用。它明确表示这份 Lambda 不读取外部变量,编译器也不会为它分配闭包。误用外面的局部变量会直接编译失败:
int factor = 2;
Func<int, int> scale = static x => x * 2;
Func<int, int> scaled = x => x * factor;
// static x => x * factor; 不能捕获 factor委托类型
Lambda 本身没有类型,要赋给一个委托,编译器才能确定参数和返回值。日常最常用的是框架自带的泛型委托。
| 委托 | 返回值 | 典型用途 | 赋值 |
|---|---|---|---|
Action | 无 | 无参回调 | Action log = () => Console.WriteLine("ok"); |
Action<T> | 无 | 处理一个参数 | Action<string> print = s => Console.WriteLine(s); |
Func<T, TResult> | 有 | 计算并返回结果 | Func<int, int> inc = n => n + 1; |
Predicate<T> | bool | 判断是否符合条件 | Predicate<int> isEven = n => n % 2 == 0; |
Predicate<T> 与 Func<T, bool> 签名相同。List<T>.Find、RemoveAll 的参数类型是 Predicate<T>。
应用示例
集合查找、删除和排序
var scores = new List<int> { 58, 76, 91, 43, 88 };
int passed = scores.Find(n => n >= 60); // 76
scores.RemoveAll(n => n < 60); // 剩下 76, 91, 88
scores.Sort((a, b) => b.CompareTo(a)); // 91, 88, 76Find 返回第一个大于等于 60 的分数;找不到时返回 default,int 就是 0。RemoveAll 删掉不及格项。Sort 的比较器返回负数、零或正数,这里用 b.CompareTo(a) 排成降序。
LINQ
同一组数据上做判断、筛选、排序和投影:
var people = new[]
{
new { Name = "Ada", Age = 36 },
new { Name = "Bob", Age = 17 },
new { Name = "Cid", Age = 28 },
};
bool hasMinor = people.Any(p => p.Age < 18);
var names = people
.Where(p => p.Age >= 18)
.OrderBy(p => p.Name)
.Select(p => p.Name);hasMinor 为 true。names 依次是 Ada、Cid:先留下成年人,按名字排序,再只取出名字。项目启用了隐式 using 时,System.Linq 已经引入;否则在文件顶部加上 using System.Linq;。
事件订阅
每次写出来的匿名 Lambda 都是一个新的委托实例。退订必须对上当初订阅的那一个,所以先把 Lambda 存进变量:
EventHandler onClick = (s, e) => Console.WriteLine("clicked");
button.Click += onClick;
button.Click -= onClick;button.Click += (s, e) => ... 可以完成订阅。这段 Lambda 没有变量可引用,之后再写一段看起来一样的 Lambda 做 -=,卸不掉原来的处理函数。
闭包与循环变量
Lambda 捕获的是变量,捕获的那一刻并不会把值复制进去。for 的循环变量在整个循环里是同一个,循环结束后再调用,读到的是最终值:
var actions = new List<Action>();
for (int i = 0; i < 3; i++)
{
actions.Add(() => Console.WriteLine(i));
}
foreach (Action action in actions)
{
action();
}上面会打印三次 3。每次迭代把当前值放进一个新的局部变量,Lambda 捕获的就是这一次的副本:
var actions = new List<Action>();
for (int i = 0; i < 3; i++)
{
int copy = i;
actions.Add(() => Console.WriteLine(copy));
}这时依次打印 0、1、2。foreach 从 C# 5 起每次迭代使用独立变量,一般不会出现这个问题。
使用注意
- 逻辑变长、需要名字表达意图,或需要打断点时,改成普通方法,再把方法传给委托。
- 闭包会让被捕获的局部变量一直活到这份委托还被引用为止。不要在 Lambda 里抓住本应结束生命周期的大对象。
- 确定不使用外部变量时写成
staticLambda。误捕获会在编译期暴露,也避免一次无谓的闭包分配。