Lambda 表达式基础
本教程共 100 篇 · 第 58 篇 · 更新于 2026-07-31 · 约 9 分钟阅读
58. Lambda 表达式基础
本节目标:学完你能写出
x => x * 2这样的 Lambda,分清参数与语句体,并理解它和委托的关系。
之前你学过委托(delegate)。委托把方法当成值传来传去,很灵活。可每次为了「传一段小逻辑」都得先声明方法,挺啰嗦。
Lambda 表达式(lambda expression) 就是写给委托「喂」的一段匿名逻辑。它把「方法」压缩成一行,专治这种小场合。
一、最直观的样子
x => x * 2 就是一个 Lambda:
namespace CSharpDemo;
Func<int, int> doubleIt = x => x * 2;
Console.WriteLine(doubleIt(5)); // 10
箭头 => 左边是参数 x,右边是「返回什么」。doubleIt(5) 把 5 喂进去,得到 10。
Note
=>读作「goes to」,字面意思「x 进去,出来 x * 2」。记住它和>=没关系,只是长得像。
二、参数怎么写
参数可以是一个、多个,也可以不写类型——编译器自己推断:
namespace CSharpDemo;
Func<int, int> square = n => n * n; // 一个参数,省略括号
Func<int, int, int> add = (a, b) => a + b; // 多个参数,必须加括号
Func<string> greet = () => "你好"; // 没有参数,写空括号
单个参数时圆括号可省;多个或零个时必写。参数类型一般不用标,因为右边目标(Func<int,int>)已经定了。
Tip类型推断不是「猜」——它看你把 Lambda 赋给了什么委托。赋值目标一明确,参数类型就定了。
参数还能用「弃元(discard)」_ 表示这个位置不关心。比如 (a, _) => a 只取第一个参数;_ 只是占位,编译器不会给它真正命名。签名要对上、又用不到某个入参时,它比随便起个名更诚实。
三、语句体:不止一行
箭头右边可以是一个表达式(表达式体),也可以用大括号写多句(语句体):
namespace CSharpDemo;
Func<int, string> describe = n =>
{
if (n < 0) return "负数";
if (n == 0) return "零";
return "正数";
};
Console.WriteLine(describe(3)); // 正数
Console.WriteLine(describe(-1)); // 负数
表达式体直接拿结果当返回值;语句体要自己写 return。逻辑一复杂,就用大括号展开。
四、和委托的关系
Lambda 自己不是类型,它是「能变成委托实例」的写法。本质上有两步:
namespace CSharpDemo;
// 1)Lambda 赋给委托变量
Func<int, int> f = x => x + 1;
// 2)直接当实参传给要委托的方法
List<int> numbers = [1, 2, 3, 4];
var bigger = numbers.Select(x => x * 10);
foreach (var n in bigger) Console.WriteLine(n); // 10 20 30 40
Select 期望一个 Func<int,int>,你的 Lambda 正好补上。Lambda 在编译期会被转成委托(或表达式树,第 60 章讲),所以「Lambda 是委托的快捷写法」这个说法很贴切。
Note没目标类型时,Lambda 自己没法单独存在。比如光写
var g = x => x * 2;会报错——编译器不知道 x 是什么。
五、类型推断的细节
编译器靠「目标」推断参数和返回类型。下面几种写法等价:
namespace CSharpDemo;
Func<int, int> a = x => x * 2; // 最常用
Func<int, int> b = (int x) => x * 2; // 显式标类型(不必须)
Func<int, int> c = (x) => { return x * 2; }; // 语句体写法
当上下文清楚,省略类型最清爽;类型模糊、或想强调,可显式写出。两种都能跑,风格任选。
六、Lambda 与匿名方法
早年 C# 用「匿名方法」写类似逻辑,关键字 delegate:
namespace CSharpDemo;
// 旧写法(了解即可)
Func<int, int> old = delegate (int x) { return x * 2; };
// 新写法
Func<int, int> ne = x => x * 2;
匿名方法啰嗦,Lambda 更短更直观。现在几乎全用 Lambda,匿名方法只在极老代码里见到。
七、为什么需要 Lambda:对比具名方法
没有 Lambda 时,为了给 Select 传一段逻辑,你得先单独声明一个方法:
namespace CSharpDemo;
// 没有 Lambda:先声明方法,再传方法名
int Double(int x) => x * 2;
List<int> list = [1, 2, 3];
var r1 = list.Select(Double);
// 有 Lambda:逻辑就地写,紧贴使用处
var r2 = list.Select(x => x * 2);
两者结果一样,但 Lambda 把「逻辑」和「使用逻辑的地方」贴在一起,读代码不用跳来跳去找方法定义。逻辑越短、越专一,这种「就地写」的优势越明显。
Tip当一段逻辑只在这一个地方用、且很短,Lambda 比具名方法更合适;逻辑要复用或很长,抽成具名方法反而更清楚。
八、方法组转换:已有方法直接传
如果你已经有一个签名匹配的现成方法,连 Lambda 的箭头都不用写,直接传方法名即可——这叫方法组转换(method group conversion):
namespace CSharpDemo;
List<int> numbers = [1, 2, 3];
// 下面两种等价,第二种更短
numbers.ForEach(n => Console.WriteLine(n));
numbers.ForEach(Console.WriteLine); // 直接传方法组
只要方法的参数和返回类型对得上目标委托,编译器就能把「方法名」转成「委托实例」。这比 n => Console.WriteLine(n) 更省字,也少一层包装。
九、Lambda 的局限
Lambda 虽好,也有几个边界要知道:
- 不能用
ref/out参数:箭头左边没法声明引用参数,需要ref/out语义时请改用本地函数(local function,见第 52 章)。 - 不能被重载:Lambda 没有名字,编译器靠目标类型区分,写不出「两个同名 Lambda」这种事。
- 递归不方便:想在自己内部调用自己,得先把 Lambda 存进变量且类型能被推断,不如本地函数直接写递归自然。
- 语句体忘写
return:表达式体自动返回,大括号里得手动return,否则返回默认值甚至编译错。
Warning不要在 Lambda 里写有副作用、却又依赖执行时机的复杂逻辑——比如边遍历边修改原集合。这类逻辑可读性差,也容易和闭包(第 59 章)的捕获规则缠在一起出 bug。
十、Lambda 与本地函数怎么选
两者都能写「一段可调用逻辑」,区别在能力和意图:
| 维度 | Lambda | 本地函数 |
|---|---|---|
| 写法 | 一行或几行,箭头直观 | 有方法名和完整签名 |
| 递归 | 不方便 | 天然支持 |
ref / out 参数 | 不支持 | 支持 |
| 捕获外部变量 | 支持(闭包) | 支持(闭包) |
| 栈跟踪 | 匿名,难定位 | 有名字,易排查 |
经验法则:一次性、短小、就地消费的过滤 / 映射逻辑,用 Lambda;稍长、要复用、要递归、要用 ref/out 的,用本地函数。
十一、常见坑
第一个坑:以为 Lambda 能脱离委托独立声明。它必须「有个家」(赋值给委托或当参数)。
第二个坑:在语句体里忘写 return。表达式体自动返回;大括号里要手动 return,否则返回默认值甚至编译错。
第三个坑:把 => 写成 >= 或 ==。=> 是 Lambda 运算符,别和比较运算符混了。
Tip写 Lambda 时,先想清楚「我要传给谁的什么委托」,参数和返回类型就自动清楚了,不必死记语法。
十二、C# 新特性:static Lambda 与自然类型
顺着 C# 的演进,Lambda 还有两个值得知道的点。
static Lambda(C# 9 起):加 static 修饰,强迫这个 Lambda「不捕获任何外部变量」。一旦它想碰外面的变量,编译器直接报错。好处是明确告诉读代码的人:这段逻辑完全自给自足,没有隐藏依赖。
namespace CSharpDemo;
int k = 10;
// 合法:捕获了外部变量
Func<int, int> a = x => x + k;
// 错误:static Lambda 不能捕获外部变量
// Func<int, int> b = static x => x + k;
自然类型(natural type,C# 10 起):早年 Lambda 必须有明确的委托目标才能存在。C# 10 后,编译器能给「参数类型完整」的 Lambda 推断出一个「自然委托类型」,于是可以写 var:
namespace CSharpDemo;
// 参数类型齐全,编译器推断出 Func<int,int>
var f = (int x) => x * 3;
Console.WriteLine(f(4)); // 12
注意:参数类型必须写全,编译器才能推出自然类型;缺了类型它依旧不知道 x 是什么。
Tip日常写 LINQ 时你几乎不用管这些——目标委托都在
Select、Where的签名里定好了。static Lambda 和 自然类型,是当你想把 Lambda 单独存成变量时才用得上。
十三、Lambda 也能配 Action / 事件
Lambda 不只能进 Func,也能进 Action(无返回值)和事件:
namespace CSharpDemo;
Action<string> log = msg => Console.WriteLine($"[日志] {msg}");
log("启动");
Action? onClick = () => Console.WriteLine("被点击了");
onClick?.Invoke();
Action 版不需要 return,箭头右边直接是「要做的事」。事件订阅也常用 Lambda 一行搞定,只是别忘了:想取消订阅,得保留同一个 Lambda 引用(第 56 章提过这个坑)。
十四、动手练一练
- 写一个
Func<int, int> cube = x => x * x * x;,调用cube(3)验证结果。 - 用
List<int>配合Where(x => x % 2 == 0)筛出偶数,再用Select(x => x * x)平方,打印结果。 - 故意把
x => x * 2写成var g = x => x * 2;观察是否报错,理解「Lambda 必须有目标类型」;再改成Func<int,int> g = x => x*2;修复。 - 把上面第 2 题的
x => x * x换成「已存在的具名方法Square」,用方法组转换Select(Square)实现,体会两种写法。
十五、小结
x => x * 2 是 Lambda 的缩影:箭头左参数、右逻辑。参数可省类型靠推断,逻辑复杂用大括号写语句体。Lambda 本质是委托的快捷写法,专治「一小段逻辑」的传递。它比具名方法更贴使用处,比匿名方法更短;已有匹配方法时还能用方法组转换直接传名。记住它的局限(无 ref/out、不便递归),并学会和本地函数分工。C# 9/10 又加了 static Lambda 与自然类型,让它能更独立地存在。下一章看它怎么「记住」外面的变量——闭包。