CodeGym /课程 /C# SELF /事件处理:lambda 表达式

事件处理:lambda 表达式

C# SELF
第 52 级 , 课程 4
可用

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 无法回收它,即使没有其它对该对象的引用也一样。结果会出现内存泄露和悬挂依赖,尤其是当发布者活得很久(静态事件、单例、服务)时。
如何避免:在释放/销毁时总是取消订阅(例如在 DisposeOnDisableOnDestroy 中)。考虑使用弱引用(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 内只保留小的包装/转发逻辑。这样能提高可读性和可维护性。

1
调查/小测验
C#中的事件第 52 级,课程 4
不可用
C#中的事件
用delegate创建事件
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION