1. 介绍
在很多现代的 C# 项目里,几乎看不到用“传统”方法处理事件的情况。原因是 lambda 表达式允许你在订阅点(操作符 +=)把处理器快速、简洁地写出来,适合那些简单且不在别处复用的处理。就像把一张小便签贴在咖啡机上写着「按这个按钮」,而不是写一份详细的手册并把它放到单独的文件夹里。如果任务是局部且一次性的,lambda 就是理想选择!
实际应用场景
- 在 ASP.NET(比如处理页面生命周期事件),
- 在 WPF/WinForms 的 UI(比如按钮点击),
- 在服务器端编程(比如 pipeline 内的逻辑),
- 在测试中,当处理器不需要单独的名字时。
语法:代码是怎么写的
先看一下常规的事件处理:
// 声明事件
public event EventHandler? MyEvent;
// 用常规方法订阅事件
void Handler(object? sender, EventArgs e)
{
Console.WriteLine("事件发生了!");
}
public void Subscribe()
{
MyEvent += Handler;
}
现在——用 lambda(匿名函数)的写法:
public void Subscribe()
{
MyEvent += (sender, e) => Console.WriteLine("事件发生了(lambda)!");
}
注意:我们不创建单独的方法,而是在订阅点直接提供处理器。lambda 的签名会自动匹配事件的类型(EventHandler)。
比较两种方式
| 普通方法 | lambda | |
|---|---|---|
| 代码量 | 更多(方法 + 订阅) | 更少,都在同一处 |
| 可复用性 | 可以复用 | 通常不能 |
| 代码局部性 | 分散 | 都在一起 |
| 清晰度/可读性 | 适合复杂逻辑 | 适合简单场景 |
2. 用事件和 lambda 写的示例应用
基本结构
public class Menu
{
public event EventHandler? ItemSelected;
public void SelectItem(int index)
{
Console.WriteLine($"菜单项 {index} 被选中。");
ItemSelected?.Invoke(this, EventArgs.Empty);
}
}
使用 lambda 订阅
class Program
{
static void Main()
{
var menu = new Menu();
// 通过 lambda 订阅事件
menu.ItemSelected += (sender, e) =>
{
Console.WriteLine("感谢你的选择!lambda 处理器已触发。");
};
menu.SelectItem(1);
}
}
预期输出:
菜单项 1 被选中。
感谢你的选择!lambda 处理器已触发。
从外部上下文捕获变量
lambda 的一个好处是它可以“记住”外部作用域(scope)里的值(闭包)。比如统计菜单被选了多少次:变量 counter 会被闭包捕获。
static void Main()
{
var menu = new Menu();
int counter = 0;
menu.ItemSelected += (s, e) =>
{
counter++;
Console.WriteLine($"已选择 {counter} 次!");
};
menu.SelectItem(1);
menu.SelectItem(2);
}
预期输出:
菜单项 1 被选中。
已选择 1 次!
菜单项 2 被选中。
已选择 2 次!
这就是闭包的魔力:变量 counter 在 lambda 内部继续存在!
带事件参数的例子
如果你的事件使用 EventHandler<T>,其中 T 是包含额外信息的自定义类,lambda 会自动匹配需要的签名。
public class MenuItemSelectedEventArgs : EventArgs
{
public int ItemIndex { get; }
public string Description { get; }
public MenuItemSelectedEventArgs(int itemIndex, string description)
{
ItemIndex = itemIndex;
Description = description;
}
}
public class Menu
{
public event EventHandler<MenuItemSelectedEventArgs>? ItemSelected;
public void SelectItem(int index, string description)
{
Console.WriteLine($"菜单项 {index}: {description} 被选中。");
ItemSelected?.Invoke(this, new MenuItemSelectedEventArgs(index, description));
}
}
// 用法
static void Main()
{
var menu = new Menu();
// 在 lambda 中使用事件参数
menu.ItemSelected += (sender, args) =>
{
Console.WriteLine($"已选择项 #{args.ItemIndex}: {args.Description.ToUpper()}");
};
menu.SelectItem(3, "关于程序");
}
预期输出:
菜单项 3: 关于程序 被选中。
已选择项 #3: 关于程序
3. 局部处理器和 lambda
lambda 很适合当:
- 处理逻辑简短且清晰,
- 处理器只在一个地方使用,
- 需要从本地上下文“捕获”变量。
如果处理逻辑复杂、需要复用或可能在声明位置之外被调用,最好用命名方法。
示例:内联逻辑和外部逻辑
lambda(理想情况):
button.Click += (s, e) => MessageBox.Show("按钮被按下!");
方法(当逻辑复杂或需要复用时):
button.Click += Button_Click;
void Button_Click(object sender, EventArgs e)
{
if (UserConfirmed())
{
SaveData();
MessageBox.Show("数据已保存!");
}
}
4. 底层:lambda 处理器发生了什么
lambda 本身也是 delegate(委托),和普通处理器一样。编译器会在背后生成一个匿名方法,如果 lambda 捕获了变量,还会生成一个隐藏的类来保存这些变量。
重要(常见错误):如果你在循环内创建 lambda 并订阅事件,所有迭代可能会捕获同一个循环变量。
for (int i = 0; i < 5; i++)
{
buttons[i].Click += (sender, e) =>
{
Console.WriteLine($"点击按钮 #{i}");
};
}
对于所有处理器,按钮编号可能都变成了 5!为避免这种情况,在循环内创建变量的副本:
for (int i = 0; i < 5; i++)
{
int buttonIndex = i; // 本地副本
buttons[i].Click += (sender, e) =>
{
Console.WriteLine($"点击按钮 #{buttonIndex}");
};
}
现在一切按预期工作。
lambda 处理器的威力:真实世界
这种写法能节省时间并加快开发,尤其是逻辑简单时。它让代码“更贴近任务”,不需要把逻辑散落在不同的文件和类里。在真实项目里你会经常碰到它们:从 UI 事件处理到在异步事件总线里订阅消息。
5. 常见错误
错误 №1:在长生命周期对象上忘记取消订阅(-=)。
如果订阅者没有从发布者那里取消订阅,delegate 的引用会把订阅者留在内存中 —— GC 无法回收它,即使没有其它对该对象的引用也一样。结果会出现内存泄露和悬挂依赖,尤其是当发布者活得很久(静态事件、单例、服务)时。
如何避免:在释放/销毁时总是取消订阅(例如在 Dispose、OnDisable、OnDestroy 中)。考虑使用弱引用(weak events / WeakEventManager)、事件管理器模式,或者在复杂场景下使用 IObservable/Rx。
错误 №2:在循环中捕获变量而不做本地副本。
典型陷阱是把订阅写在循环里,闭包捕获同一个循环变量,结果所有处理器看到的都是它的最终值,而不是创建处理器时的那个值。这会导致意外行为(所有处理器输出相同的数字等)。
如何避免:在循环内创建本地副本并捕获它:
for (int i = 0; i < n; i++)
{
int current = i;
button.Click += (s, e) => Handle(current);
}
或者把需要的值作为参数传给包装方法。这更可靠并且意图更清晰。
Error №3: 使用臃肿的 lambda 在订阅点写大量逻辑。
如果你把大型业务逻辑直接放进匿名函数,代码会变得难读、难测、难调试。另外,用匿名 lambda 更难正确取消订阅,因为需要同一个 delegate 实例。结果逻辑四散在各处、失去结构。
如何避免:把复杂逻辑抽到命名方法或服务;必要时把 delegate 存到变量或字段,以便能够取消订阅;在 lambda 内只保留小的包装/转发逻辑。这样能提高可读性和可维护性。
GO TO FULL VERSION