55

我在 C# 中工作,我的工作场所有一些代码标准。其中之一是我们连接的每个事件处理程序(例如KeyDown)必须在Dispose方法中断开连接。有什么好的理由吗?

4

4 回答 4

91

除非您希望事件的发布者比订阅者更长寿,否则没有理由删除事件处理程序,不。

这是民间传说已经发展起来的主题之一。你真的只需要用正常的术语来考虑它:发布者(例如按钮)有一个对订阅者的引用。如果发布者和订阅者无论如何都可以同时进行垃圾回收(这很常见),或者如果发布者更早有资格进行垃圾回收,那么就不存在 GC 问题。

静态事件会导致 GC 问题,因为它们实际上是一个无限长寿命的发布者——我会在可能的情况下完全阻止静态事件。(我很少发现它们有用。)

另一个可能的问题是如果您明确想要停止侦听事件,因为如果引发事件,您的对象会行为不端(例如,它将尝试写入关闭的流)。在这种情况下,是的,您应该删除处理程序。这很可能是您的类IDisposable已经实现的情况。这将是不寻常的 - 尽管并非不可能 - 值得实现IDisposable 只是为了删除事件处理程序。

于 2013-07-01T08:19:16.027 回答
14

好吧,也许,该标准是作为一种针对内存泄漏的防御性实践而提出的。我不能说,这是一个糟糕的标准。但是,我个人更喜欢仅在需要时断开事件处理程序。这样,我的代码看起来干净且不那么冗长。

我写了一篇博客,解释事件处理程序如何导致内存泄漏以及何时断开连接;https://www.spicelogic.com/Blog/net-event-handler-memory-leak-16。在这里,我将总结解释以解决您的核心问题。

C# Event Handler 操作符实际上是一个引用注入器:

在 C# 中,+= 运算符看起来非常天真,许多新开发人员不知道右侧对象实际上是在传递它是对左侧对象的引用。

在此处输入图像描述

事件发布者保护事件订阅者:

那么,如果一个对象获得了对另一个对象的引用,那有什么问题呢?问题是,当垃圾收集器开始清理并找到一个重要的对象以保留在内存中时,它不会清理该重要对象也引用的所有对象。让我简单点。假设您有一个名为“客户”的对象。比如说,这个客户对象有一个对 CustomerRepository 对象的引用,这样客户对象就可以在存储库中搜索它的所有 Address 对象。因此,如果垃圾收集器发现需要客户对象处于活动状态,那么垃圾收集器也会保持客户存储库处于活动状态,因为客户对象具有对 customerRepository 对象的引用。这是有道理的,因为客户对象需要customeRepository 对象才能运行。

但是,事件发布者对象是否需要事件处理程序才能运行?没有权利?事件发布者独立于事件订阅者。事件发布者不应该关心事件订阅者是否活着。当您使用 += 运算符订阅事件发布者的事件时,事件发布者会收到事件订阅者的引用。垃圾收集器认为,事件发布者需要事件订阅者对象才能发挥作用,所以它不收集事件订阅者对象。

这样,事件发布者对象“a”保护事件订阅者对象“b”不被垃圾收集器收集。

事件发布者对象只要事件发布者对象处于活动状态,就保护事件订阅者对象。

在此处输入图像描述

所以,如果分离事件处理程序,那么事件发布者就不会持有事件订阅者的引用,垃圾收集器可以自由地收集事件订阅者。

但是,你真的需要一直分离事件处理程序吗?答案是否定的。因为只要事件发布者存在,许多事件订阅者就应该真正存在于内存中。

做出正确决定的流程图:

在此处输入图像描述

大多数时候,我们发现事件订阅者对象与事件发布者对象一样重要,并且两者都应该同时存在。

您无需担心的场景示例:

例如,窗口的按钮单击事件。

在此处输入图像描述

这里,事件发布者是Button,事件订阅者是MainWindow。应用该流程图,问一个问题,主窗口(事件订阅者)是否应该在按钮(事件发布者)之前死掉?显然没有。对吧?这甚至没有意义。那么,为什么要担心分离点击事件处理程序呢?

当事件处理程序分离是必须的示例:

我将提供一个示例,其中订阅者对象应该在发布者对象之前死亡。比如说,您的 MainWindow 发布了一个名为“SomethingHappened”的事件,并且您通过单击按钮从主窗口显示一个子窗口。子窗口订阅主窗口的那个事件。

在此处输入图像描述

并且,子窗口订阅主窗口的事件。

在此处输入图像描述

当用户单击 MainWindow 中的按钮时,将显示子窗口。然后用户在他/她从子窗口完成任务时关闭子窗口。现在,根据我提供的流程图,如果您问一个问题“子窗口(事件订阅者)是否应该在事件发布者(主窗口)之前死亡?答案应该是肯定的。对吗?然后,确保分离子窗口任务完成时的事件处理函数,一个好地方就是 ChildWindow 的 Unloaded 事件。

验证内存泄漏的概念:

我已经使用 Jet Brains 的 dotMemory 内存分析器软件分析了此代码。我启动了 MainWindow 并单击了3 次按钮,它显示了一个子窗口。因此,出现了 3 个子窗口实例。然后,我关闭了所有子窗口,并比较了子窗口出现前后的快照。我发现孩子窗口的3 个对象仍然存在于内存中,即使我已将它们全部关闭。

在此处输入图像描述

然后,我在子窗口的 Unloaded 事件中分离了事件处理程序,如下所示:

在此处输入图像描述

然后,我再次剖析,这一次,哇!该事件处理程序不再导致内存泄漏。

在此处输入图像描述

于 2020-05-18T08:52:37.343 回答
11

如果我没有在动态创建和销毁的用户控件的 Dispose() 中取消注册事件处理程序,我的应用程序中就会发生重大 GDI 泄漏。我在 C# 编程指南的 Visual Studio 2013 帮助中找到了以下内容。注意我用斜体字写的东西:

如何:订阅和取消订阅事件

……剪……

退订

要防止在引发事件时调用您的事件处理程序,请取消订阅该事件。为了防止资源泄漏,您应该在处置订阅者对象之前取消订阅事件。在您取消订阅某个事件之前,发布对象中作为该事件基础的多播委托具有对封装订阅者事件处理程序的委托的引用。只要发布对象持有该引用,垃圾回收就不会删除您的订阅者对象。

请注意,在我的例子中,发布者和订阅者都在同一个类中,并且处理程序不是静态的。

于 2016-05-19T10:16:50.120 回答
1

我面临的一个原因是它影响了程序集的可卸载性

于 2021-03-05T17:36:11.617 回答