首页 / C# 入门教程 / Lambda 表达式基础

C# 入门教程

Lambda 表达式基础

本教程共 100 篇 · 第 58 篇 · 更新于 2026-07-31 · 约 9 分钟阅读

C#C# 入门教程编程语言Lambda委托匿名方法

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 虽好,也有几个边界要知道:

  1. 不能用 ref / out 参数:箭头左边没法声明引用参数,需要 ref/out 语义时请改用本地函数(local function,见第 52 章)。
  2. 不能被重载:Lambda 没有名字,编译器靠目标类型区分,写不出「两个同名 Lambda」这种事。
  3. 递归不方便:想在自己内部调用自己,得先把 Lambda 存进变量且类型能被推断,不如本地函数直接写递归自然。
  4. 语句体忘写 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 时你几乎不用管这些——目标委托都在 SelectWhere 的签名里定好了。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 章提过这个坑)。

十四、动手练一练

  1. 写一个 Func<int, int> cube = x => x * x * x;,调用 cube(3) 验证结果。
  2. List<int> 配合 Where(x => x % 2 == 0) 筛出偶数,再用 Select(x => x * x) 平方,打印结果。
  3. 故意把 x => x * 2 写成 var g = x => x * 2; 观察是否报错,理解「Lambda 必须有目标类型」;再改成 Func<int,int> g = x => x*2; 修复。
  4. 把上面第 2 题的 x => x * x 换成「已存在的具名方法 Square」,用方法组转换 Select(Square) 实现,体会两种写法。

十五、小结

x => x * 2 是 Lambda 的缩影:箭头左参数、右逻辑。参数可省类型靠推断,逻辑复杂用大括号写语句体。Lambda 本质是委托的快捷写法,专治「一小段逻辑」的传递。它比具名方法更贴使用处,比匿名方法更短;已有匹配方法时还能用方法组转换直接传名。记住它的局限(无 ref/out、不便递归),并学会和本地函数分工。C# 9/10 又加了 static Lambda 与自然类型,让它能更独立地存在。下一章看它怎么「记住」外面的变量——闭包。