我在C#工作,我的工作场所有一些代码标准。其中之一是我们连接的每个事件处理程序(例如KeyDown
)必须在Dispose
方法中断开连接。这有什么好的理由吗?
答案 0 :(得分:70)
除非您希望活动的发布者比订阅者活得更长,否则没有理由删除事件处理程序,不。
这是民间传说成长的话题之一。您只需要以正常的方式考虑它:发布者(例如按钮)具有对订阅者的引用。如果发布者和订阅者无论如何都有资格同时进行垃圾收集(如常见),或者如果发布者有资格进行之前的垃圾收集,那么就没有GC问题。
静态事件导致GC问题,因为它们实际上是一个无限长期的发布者 - 我会在可能的情况下完全阻止静态事件。 (我很少发现它们有用。)
另一个可能的问题是,如果您明确要停止侦听事件,因为如果引发事件,您的对象将出现异常(例如,它将尝试写入已关闭的流)。在这种情况下,是的,你应该删除处理程序。这种情况最有可能是您的类已经实现IDisposable
的情况。值得实现IDisposable
以删除事件处理程序是不寻常的 - 尽管不是不可能的。
答案 1 :(得分:11)
如果我没有注销动态创建和销毁的用户控件的Dispose()中的事件处理程序,那么我的应用程序中会出现主要的GDI泄漏。我在C#编程指南的Visual Studio 2013帮助中找到了以下内容。请注意我用斜体字表示的内容:
如何:订阅和取消订阅活动
...略...的取消订阅强>
要防止在引发事件时调用事件处理程序,请取消订阅该事件。 为了防止资源泄漏,您应该在处置订阅者对象之前取消订阅事件。在您取消订阅事件之前,发布对象中作为事件基础的多播委托具有对封装订阅者事件处理程序的委托的引用。只要发布对象包含该引用,垃圾回收就不会删除您的订阅者对象。
请注意,在我的情况下,发布者和订阅者都在同一个类中,处理程序不是静态的。
答案 2 :(得分:5)
也许,该标准是作为针对内存泄漏的防御措施提出的。我不能说,这是一个不好的标准。但是,我个人更喜欢仅在需要时才断开事件处理程序。这样,我的代码看起来干净整洁,不那么冗长。
我写了一篇博客,解释事件处理程序如何导致内存泄漏以及何时断开连接; https://www.spicelogic.com/Blog/net-event-handler-memory-leak-16。在这里,我将总结说明以解决您的核心问题。
C#事件处理程序运算符实际上是参考注入器:
在C#+ =中,运算符看起来很无辜,许多新开发人员没有想到右侧对象实际上是在传递它是对左侧对象的引用。
事件发布者保护事件订阅者:
因此,如果一个对象获得了对另一个对象的引用,那是什么问题?问题在于,当垃圾收集器清理并找到一个重要的对象要保留在内存中时,它不会清理该重要对象也引用的所有对象。让我简单点。假设您有一个名为“客户”的对象。说,此客户对象具有对CustomerRepository对象的引用,以便客户对象可以在存储库中搜索其所有Address对象。因此,如果垃圾收集器发现需要使客户对象保持活动状态,则垃圾收集器还将使客户存储库保持活动状态,因为客户对象具有对customerRepository对象的引用。这很有意义,因为客户对象需要customeRepository对象才能起作用。
但是,事件发布者对象是否需要事件处理程序才能起作用?无权利?事件发布者独立于事件订阅者。事件发布者不必关心事件订阅者是否还活着。当您使用+ =运算符订阅事件发布者的事件时,事件发布者会收到事件订阅者的引用。垃圾收集器认为,事件发布者需要事件订阅者对象才能运行,因此它不收集事件订阅者对象。
通过这种方式,事件发布者对象“ a”保护事件订阅者对象“ b”不被垃圾收集器收集。
事件发布者对象只要事件发布者对象还处于活动状态就可以保护事件订阅者对象。
因此,如果分离事件处理程序,则事件发布者将不持有事件订阅者的引用,并且垃圾回收器可以自由地收集事件订阅者。
但是,您是否真的需要一直分离事件处理程序?答案是否定的。因为只要事件发布者存在,许多事件订阅者实际上就应该活在内存中。
做出正确决定的流程图:
大多数时候,我们发现事件订阅者对象与事件发布者对象同等重要,并且两者都应该同时存在。
您无需担心的情况示例:
例如,窗口的按钮单击事件。
在这里,事件发布者是Button,事件订阅者是MainWindow。应用该流程图,提出一个问题,主窗口(事件订阅者)是否应该在Button(事件发布者)之前失效?显然不是。那甚至没有意义。然后,为什么要担心分离单击事件处理程序?
必须分离事件处理程序的示例:
我将提供一个示例,其中订阅者对象应该在发布者对象之前死亡。假设您的MainWindow发布了一个名为“ SomethingHappened”的事件,并且您通过单击按钮从主窗口显示了一个子窗口。子窗口订阅了主窗口的事件。
然后,子窗口预订主窗口的事件。
当用户单击MainWindow中的按钮时,将出现子窗口。然后,当用户从子窗口完成任务时,用户将关闭子窗口。现在,根据我提供的流程图,如果您问一个问题:“子窗口(事件订阅者)是否应该在事件发布者(主窗口)之前死亡?答案应该是。对吗?然后,确保分离子窗口任务完成时的事件处理程序。一个好地方是ChildWindow的Unloaded事件。
验证内存泄漏的概念:
我已经使用Jet Brains的dotMemory内存分析器软件对此代码进行了分析。我启动MainWindow并单击 3次按钮,该按钮显示一个子窗口。因此,出现了3个子窗口实例。然后,我关闭了所有子窗口,并比较了子窗口出现之前和之后的快照。我发现子窗口的 3个对象仍存在于内存中,即使我关闭了所有这些对象。
然后,我已经在子窗口的Unloaded事件中分离了事件处理程序,如下所示:
然后,我再次进行了概要分析,这次,哇!该事件处理程序不再导致内存泄漏。
答案 3 :(得分:1)
我面临的一个原因是它影响了assembly unloadability