CodeGym /课程 /C# SELF /lambda 表达式的优点和缺点 =>

lambda 表达式的优点和缺点 =>

C# SELF
第 49 级, 课程 3
可用

1. lambda 表达式的优点

在编程里经常会重复类似的任务。比如:"筛选出年龄大于 18 的用户","计算满足条件的所有数字之和","按价格对商品排序"。没有 lambda 的时候,这些通常需要写额外的方法——这会让代码变得嘈杂,尤其当这个行为只在一个地方用到时。lambda 让代码更紧凑,更接近我们心里描述问题的方式。

lambda 表达式是很多现代语言的标准工具(不仅仅是 C#),因为它们允许把“行为”作为值来传递,不管是过滤器、处理器还是转换函数。

1. 简洁明了

lambda 可以省掉冗长的多余方法或匿名 delegate 的声明,当你只需要在一处写一小段功能时特别方便。比如在没有 lambda 之前,筛选数字列表可能是这样的:


List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6 };

// 在引入 lambda 之前:
List<int> evenNumbers = numbers.FindAll(delegate(int x) { return x % 2 == 0; });

// 使用 lambda 表达式:
List<int> evenNumbers2 = numbers.FindAll(x => x % 2 == 0);

结果是一样的,但有了 lambda 代码更简洁。在大项目里这能显著节省行数。

2. 提高可读性和表达力

lambda 能让你专注于操作本身,去掉语法噪音。代码更接近自然语言:


var adults = users.Where(user => user.Age >= 18);

和为了这个过滤写一个单独的方法 bool IsAdult(User user) 相比,就显得更直接。

3. 和 LINQ / 集合 API 的良好集成

lambda 的威力在于和 LINQ 及集合配合。很多集合方法和 LINQ 操作符都期望一个函数作为参数(比如用于过滤的 Func<T, bool>)。lambda 可以在调用处直接声明需要的函数:


var expensive = products.Where(p => p.Price > 1000);
var firstBook = books.FirstOrDefault(b => b.Title.StartsWith("C#"));
var doubled = numbers.Select(n => n * 2);

4. 从外部作用域捕获变量(closures)

lambda 可以使用外部声明的变量。这带来灵活性,允许在运行时生成带有“定制参数”的函数:


int minAge = 18;
var filtered = users.Where(u => u.Age >= minAge); // minAge 被 lambda "捕获"

这打开了用来生成“带配置的函数”的有趣模式。

有趣的事实:在 lambda 内不仅可以读取外部变量,有时候还能修改它们,但要小心——关于闭包的详细内容会在闭包专题里讲到!

5. 内嵌且上下文相关的代码

lambda 就写在使用它的地方,而不是在项目中到处找命名方法。这符合“最少空间里包含最多信息”的原则。

在我们示例的应用演进(记住,我们在开发一个简易的图书馆图书管理系统)中,假设我们有如下书籍列表:


public class Book
{
    public string Title { get; set; }
    public string Author { get; set; }
    public int Year { get; set; }
    public double Price { get; set; }
}

// 在代码的某处:
List<Book> books = new List<Book>
{
    new Book { Title = "C# 9.0 in a Nutshell", Author = "Skeet", Year = 2022, Price = 350 },
    new Book { Title = "CLR via C#", Author = "Richter", Year = 2019, Price = 250 },
    // ...
};

// 找到所有价格大于 300 的书:
var expensiveBooks = books.Where(b => b.Price > 300).ToList();

你可以在调用处直接看到筛选条件,不用去代码其他地方找辅助函数。

6. 用作回调、事件、计时器

lambda 非常适合用于一次性的操作,比如事件处理器(回调):


button.Click += (sender, args) => Console.WriteLine("按钮被按了!");

如果处理器很简单,lambda 可以省去写单独方法的麻烦。

7. 扩展 delegate 的能力

以前要传递行为必须写命名方法;现在可以在调用地点直接写出函数体:


Timer timer = new Timer(_ => Console.WriteLine("Tick!"), null, 0, 1000);

8. 简化测试和依赖注入

通过 lambda,可以很容易地为测试创建假的(mock)行为实现,而不污染主代码库或添加临时类。比如构造函数接受一个 delegate,测试里就传入一个符合需求的 lambda。

2. lambda 表达式的主要缺点

像任何强力工具一样,lambda 也不是完美的。下面讨论它们可能带来的困难和限制。

1. 过度嵌套会导致可读性下降

lambda 很棒,但当一个地方出现太多 lambda 就不行了。嵌套的 lambda 或者很长的 lambda 会让代码难以理解:


var result = items.Select(x => x.Children.Where(y => y.Value > 10)
                         .Select(z => z.Name.ToUpper())
                         .ToList());

再加上几个层级 —— 大家就会遇到“大家的代码阅读迷题”。

建议:如果 lambda 超过 3–4 行,就把它提取成一个命名方法。别害怕被认为保守:可读性比时髦的缩写重要。

2. 调试困难

lambda 对调试友好度一般,尤其是当它们写成一行并链在 LINQ 中时。有时候难以在 lambda 内部设置断点或在某一步查看变量值。

为了方便调试,可以暂时把 lambda 的主体提取到命名方法,或者把长的 LINQ 链拆成有中间变量的几段。

3. 参数和返回类型不够直观

lambda 通常作为 delegate(Func<...>, Action<...>, Predicate<T>)传递。有时很难立刻看出输入参数和返回类型到底应该是什么,尤其在泛型方法里更容易迷糊。

例如:


Func<int, string, double> myFunc = (a, b) => a + b.Length; // 哎!这里会返回 int,但声明是 double.

编译器会报错,但对新手来说一下子理解“哪个 lambda 不匹配签名”并不容易。

4. 变量捕获带来的问题

从外部作用域捕获变量(closure)是把双刃剑。如果不注意捕获的变量,可能得到意外结果。比如在循环里:


var actions = new List<Action>();
for (int i = 0; i < 3; i++)
{
    actions.Add(() => Console.WriteLine(i));
}
foreach (var action in actions) action();

很多人期望输出 0 1 2,但实际上得到的是 3 3 3。为什么?因为在 lambda 执行时变量 i 已经等于 3!lambda 捕获的是变量本身,而不是它当时的值。

这是新手常犯的错误,官方的 文档 有详细说明。可以解决,但需要小心处理。

5. 丢失显式名字,复用困难

lambda 适合一次性使用。但如果同样的条件/函数在多个地方用到,最好把逻辑提取到命名方法。否则你可能会遇到重复代码,修改时出错。

6. 不方便写 XML 注释

不能给 lambda 添加用于自动生成文档的 XML 注释(像方法那样)。只能在代码里写普通注释来解释 lambda。

7. 可能的性能问题

大多数情况下 lambda 的性能和普通方法差别不明显。但当频繁大量创建带捕获的 lambda 时,会分配额外的对象(closure),在性能敏感的场景(比如 tight loop 或高负载服务)应考虑使用静态方法。

8. 不能在外层循环上下文里使用 goto / break / continue

如果 lambda 在循环内部声明,lambda 里不能直接对外层循环使用 break 或 continue —— 语法上不允许。

9. lambda 并不能描述所有行为

lambda 不能直接使用属性(attributes)、不能有访问修饰符,也不能做某些特殊操作——比如不能声明有名字的局部函数等。

3. 在两难之间做选择

什么时候 lambda 有用

场景 用 lambda 合适吗? 为什么
简短的过滤/转换 👍 快速且清晰
多层嵌套操作 👎 会变得不可读
Re-use (重复使用) 👎 最好提取到方法
Callback 逻辑,事件 👍 简洁
描述复杂的业务逻辑 👎 需要名字 + 注释
与 LINQ 一起工作 👍 理想场景

什么时候还是别用 lambda

  • 如果逻辑很长并且包含很多分支/计算。
  • 如果 lambda 做了对读者不直观的事情且没有说明。
  • 如果函数需要文档、要在多个地方使用或需要一个“说明性”的名字。
  • 如果 lambda 出现在过深的嵌套调用中——有丢失可读性的风险。

4. 使用 lambda 表达式时的典型错误

循环中捕获变量的错误:


List<Action> actions = new List<Action>();
for (int i = 0; i < 5; i++)
{
    actions.Add(() => Console.WriteLine(i));
}
foreach (var act in actions) act(); // 全部会输出 5!

正确做法:


for (int i = 0; i < 5; i++)
{
    int captured = i; // 捕获一个单独的变量
    actions.Add(() => Console.WriteLine(captured));
}

过长的 lambda:


books.Where(b => b.Price > 1000 && b.Title.Contains("C#") && b.Author.Length > 4 && 等等...);
// 代码变得不可读,提取成方法!

在需要带文档的方法处使用 lambda:
如果函数被多次使用或者需要详细注释,最好写一个命名方法:


bool IsExpensiveBook(Book book) => book.Price > 1000;
books.Where(IsExpensiveBook);
2
任务
C# SELF, 第 49 级, 课程 3
已锁定
使用 Lambda 转换列表
使用 Lambda 转换列表
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION