1. 介绍
在 C#(以及整个 .NET)里,事件是对象之间互相通信的一种方式。比如你有一个按钮,点击它应该触发某些操作。可以通过声明单独的方法来处理事件,然后把那个方法显式订阅到事件上……但很多时候你只想“就在这儿写反应”,别把逻辑分散到文件的各个部分。
Lambda 表达式非常适合解决这个问题 — 允许你直接在订阅事件的地方写出对事件的反应。仪式感少,效率高。
同样地,回调(callback)场景也是:有时候你把某段逻辑传给方法,等某个操作完成后再调用。Lambda 表达式让这件事简单直观:需要的逻辑就在调用处,清晰易读。
事件和回调:要点以及什么时候用 Lambda
事件 — 本质上是对象上的一个“挂钩”:别人可以订阅,当某件事发生时就会执行订阅者的代码。
Callback(回调) — 就是“我现在给你一个函数,等需要的时候你来调用它”。很像“留下联系方式,我们会回电”那种模式。
当你学过 delegate 和 event 的时候,可能见过这样的语法:
button.Click += MyButtonClickHandler;
其中 MyButtonClickHandler 是你需要在某处声明的专门方法。但当逻辑很简单时,你宁愿用几行代码搞定,而不是为一个动作额外写方法。
这时 Lambda 就登场了:
button.Click += (sender, args) => { Console.WriteLine("按钮被按下!"); };
简洁、直观、就在这里!
2. 作为事件处理器的 Lambda:从简单到稍复杂
简单形式:灯一闪 — 控制台输出
假设我们有一个类 Button,想要响应它的 Click 事件。
// 假设有这么个按钮:
public class Button
{
public event EventHandler? Click;
public void SimulateClick() // 举例:程序性地“点击”按钮
{
// 如果有人订阅了 — 调用处理器
Click?.Invoke(this, EventArgs.Empty);
}
}
现在使用这个按钮:
var button = new Button();
// 用 Lambda 订阅按钮事件!
button.Click += (sender, args) =>
{
Console.WriteLine("太棒了!按钮被按下了!");
};
button.SimulateClick(); // => 太棒了!按钮被按下了!
说明:
— 在 => 之后的是你这个“小函数”的主体,当事件发生时会被调用。
— 不用纠结处理器的生命周期:它活着只要你的订阅(+=)还在。注意:如果 Lambda 捕获了外部变量但你不取消订阅,可能会导致内存泄漏。
捕获外部变量的 Lambda 处理器
int clickCount = 0;
button.Click += (sender, args) =>
{
clickCount++;
Console.WriteLine($"按钮已经被按了 {clickCount} 次!");
};
button.SimulateClick(); // => 按钮已经被按了 1 次!
button.SimulateClick(); // => 按钮已经被按了 2 次!
发生了什么:
Lambda 捕获了外部变量 clickCount,并在多次调用之间记住它的值。
3. 把 Lambda 用作 callbacks
回调 常用于某段代码需要在将来调用你的函数,比如某个耗时操作完成后。
把 Lambda 作为 callback 传递
假设有个方法接收一个 delegate 或 Lambda 作为参数:
void DoWork(Action callback)
{
Console.WriteLine("开始工作...");
// 模拟工作
System.Threading.Thread.Sleep(500); // (真实应用别这么做,这是阻塞操作!)
callback(); // 工作结束后调用 callback
}
DoWork(() => Console.WriteLine("工作完成!"));
结果:
控制台先会输出 "开始工作...",然后是 "工作完成!"。
也可以传带参数的 Lambda:
void Calculate(int a, int b, Action<int> onResult)
{
int sum = a + b;
onResult(sum);
}
Calculate(5, 8, result => Console.WriteLine($"结果: {result}"));
4. 使用 Lambda 时的常见错误
不取消事件订阅
当你用 Lambda 订阅事件时,通常没有“处理器名称”可以方便取消订阅:
button.Click += (s, e) => Console.WriteLine("...");
button.Click -= ??? // 怎么把你的 Lambda 传进去?如果没保存,做不到。
如果确实需要取消订阅 — 把 Lambda 保存到变量:
EventHandler handler = (s, e) => Console.WriteLine("...");
button.Click += handler;
// ...然后
button.Click -= handler;
这对长生命周期对象很重要:不取消订阅会留下引用,可能导致内存泄漏。
循环中捕获变量的问题
经典情况:在循环里订阅事件:
for (int i = 0; i < 3; i++)
button.Click += (s, e) => Console.WriteLine(i); // 所有 Lambda “记住”的都是同一个 i
在新版 C# 中,foreach 的行为已经修正过,但 for 有时还会有细节问题 — 对于被捕获的变量要始终小心!
如果你希望每个 Lambda 记住各自的值 — 创建局部副本:
for (int i = 0; i < 3; i++)
{
int localI = i;
button.Click += (s, e) => Console.WriteLine(localI);
}
GO TO FULL VERSION