事件处理程序的Lambda表达式?

9
C# 3 中的 Lambda 语法使得创建单行匿名方法非常方便。相比 C# 2 中更加冗长的匿名委托语法,它们无疑是一种进步。然而,Lambda 的便利性也带来了一个诱惑:在不需要提供函数式编程语义的地方使用它们。
例如,我经常发现我的事件处理程序(或者至少起初)是简单的单行代码,用于设置状态值、调用另一个函数或设置另一个对象的属性等。对于这些情况,我应该在类中添加另一个简单函数,还是将 Lambda 填充到构造函数中的事件中呢?
在这种情况下,Lambda 有一些明显的缺点:
- 我不能直接调用事件处理程序;它只能由事件触发。当然,在这些简单的事件处理程序中,几乎没有必要直接调用它们。 - 我不能从事件中解除挂钩处理程序。另一方面,我很少需要取消挂钩事件处理程序,所以这不是什么问题。
这两个问题并不让我太困扰,因为已经有了解决办法。如果真的有问题,我可以通过将 Lambda 存储在成员委托中来解决这两个问题,但那样做会失去使用 Lambda 的便利和保持类中不杂乱的目的。
然而,还有两个问题可能不太明显,但可能更加棘手:
- 每个 Lambda 函数都形成其所在作用域的闭包。这可能意味着由于闭包维护对它们的引用,构造函数中早期创建的临时对象会比需要更长时间地存活。幸运的是,编译器应该足够聪明,可以将 Lambda 不使用的对象排除在闭包之外,但我不确定。有人知道吗? - 可能会影响可维护性。如果我将一些事件处理程序定义为函数,另一些定义为 Lambda,那么跟踪错误或理解类可能会更加困难。而且,如果我的事件处理程序最终扩展,我要么必须将它们移动到类级别的函数中,要么就必须接受构造函数现在包含实现类功能的相当数量的代码这一事实。

我希望能借鉴其他有函数式编程特点语言经验的人的建议和经验。这种情况下是否有已经确立的最佳实践?在事件处理程序或其他情况下,您会避免使用lambda表达式吗?如果不是,那么在什么阈值下您会决定使用真正的函数而不是lambda表达式?上述任何陷阱是否曾经显著影响过任何人?还有我没有想到的陷阱吗?

5个回答

4
我通常会有一个专门用于连接事件处理程序的例程。在其中,我使用匿名委托或lambda表达式作为实际处理程序,使它们尽可能短小。这些处理程序有两个任务:
  1. 解包事件参数。
  2. 使用适当的参数调用命名方法。
这样做,我避免了将事件处理程序方法混杂在类名称空间中,这些方法不能干净地用于其他目的,并迫使自己思考我所实现的操作方法的需求和目的,通常导致更清晰的代码。

+1. 这与我在本地代码中的WndProc所做的类似。 - P Daddy
嘿,是的...那就是我的出发点。 - Shog9

2
每个lambda函数都形成了对其包含范围的闭包。这可能意味着由于闭包保持对它们的引用,先前在构造函数中创建的临时对象会存活得比它们需要的时间长得多。现在希望编译器足够聪明,能够排除lambda不使用的闭包对象,但我不确定。有人知道吗?
从我所读到的内容来看,C#编译器会生成匿名方法或匿名内部类,具体取决于是否需要关闭包含范围。
换句话说,如果您不从lambda中访问包含范围,它就不会生成闭包。
然而,这是一种“传闻”,我希望有更熟悉C#编译器的人发表意见。
总之,旧版的C#2.0匿名委托语法也是同样的情况,我几乎总是在短事件处理程序中使用匿名委托。
您已经很好地涵盖了各种利弊,如果需要取消挂接事件处理程序,请不要使用匿名方法,否则我完全支持它。

C#编译器只有在lambda表达式中使用变量时才能访问其所在作用域内的变量,否则不会出现问题(这同样适用于匿名方法和lambda表达式)。 - Mehrdad Afshari

2

根据我对编译器进行的小实验,我可以说编译器足够聪明,能够创建闭包。我的实验很简单,就是一个构造函数,其中有两个不同的lambda用于List.Find()中的Predicate。

第一个lambda使用了硬编码的值,第二个则使用了构造函数中的参数。第一个lambda被实现为类的私有静态方法。第二个lambda被实现为执行封闭操作的类。

因此,您的假设是正确的,编译器确实足够聪明。


这证实了我下面怀疑的事情。感谢您检查IL。 - FlySwat
没问题,我也觉得这样会行,但出于好奇心想确认一下。 - JoshBerke

1

大多数Lambda的相同特性同样适用于其他可以使用它们的地方。如果事件处理程序不是一个适合它们的地方,我想不出更好的地方了。它是一个位于其单个点的单一自包含逻辑单元。

在许多情况下,该事件旨在获取一个小的上下文包,结果正好适合手头的工作。

在重构意义上,我认为这是“良好特征”之一。


0
关于lambda表达式,我最近提出的这个问题在被接受的答案中有一些有关对象生命周期影响的相关事实。
另一个我最近学到的有趣的事情是,C#编译器将同一范围内的多个闭包视为单个闭包,以尊重其捕获和保持活动状态的内容。不幸的是我找不到原始来源。如果我再次遇到它,我会添加的。
就我个人而言,我不使用lambda表达式作为事件处理程序,因为我认为当逻辑从请求流向结果时,可读性的优势才真正显现。事件处理程序往往会在构造函数或初始化器中添加,但是在对象的生命周期的这个阶段很少会被调用。那么为什么我的构造函数应该看起来像正在进行实际上会发生得晚得多的事情呢?
另一方面,我确实使用了略微不同类型的事件机制,我认为比C#语言特性更可取:一个在C#中重写的类似iOS风格的NotificationCenter,其中包含由类型(从Notification派生)密钥化的调度表和Action < Notification >值。这最终允许单行“事件”类型定义,如下所示:
public class UserIsUnhappy : Notification { public int unhappiness; }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接