您需要在析构函数中移除事件处理程序吗?

72

我在运行时使用一些UserControls,这些控件在我的应用程序中创建和销毁(通过创建并关闭包含这些控件的子窗口)。
这是一个WPF UserControl,并继承自System.Windows.Controls.UserControl。它没有Dispose()方法可以重写。
PPMM是一个与我的应用程序具有相同生命周期的Singleton
现在在我的(WPF)UserControl构造函数中,我添加了一个事件处理程序:

public MyControl()
{
    InitializeComponent();

    // hook up to an event
    PPMM.FactorChanged += new ppmmEventHandler(PPMM_FactorChanged);
}

我已经习惯在析构函数中删除这样的事件处理程序:

~MyControl()
{
    // hook off of the event
    PPMM.FactorChanged -= new ppmmEventHandler(PPMM_FactorChanged);
}

今天我偶然发现这个问题并想问:

1) 这是必要的吗?或者GC会处理它?

2) 这是否有效?或者我需要存储新创建的ppmmEventHandler?

期待您的答案。


2
这里有一个完美的解释:https://dev59.com/h3M_5IYBdhLWcg3wp06l - V4Vendetta
9个回答

47

由于PPMM是一个长寿命对象(单例模式),因此这段代码并没有太多意义。

问题在于只要该事件处理程序引用该对象,它就不会有资格进行垃圾回收,至少在拥有该事件的其他对象仍然存活时是这样的。

因此,在析构函数中放置任何内容都是没有意义的,因为:

  1. 事件处理程序已被移除,因此对象变得可以进行垃圾回收
  2. 事件处理程序没有被移除,拥有它的对象不具备进行垃圾回收的条件,因此终结器永远不会被调用
  3. 两个对象都可以进行垃圾回收,在这种情况下,由于不知道另一个对象的内部状态,因此在终结器中根本不应访问该对象

简而言之,不要这样做

现在,对于添加这样的代码到Dispose方法中的不同论点,当您正在实现IDisposable时,这完全有意义,因为它是用户代码在预定义和可控点上调用Dispose

然而,仅在对象有资格进行垃圾回收并且具有终结器时才调用终结器(析构函数),在这种情况下是没有意义的。

至于第二个问题,我认为它是“我可以这样取消订阅事件吗”,那么是的,您可以。您需要持有使用匿名方法或lambda表达式构建委托时使用的委托对象的唯一时间是在构建委托对象周围时。当您将其构建在现有方法周围时,它将起作用。

编辑:WPF。没错,我没有看到那个标签。抱歉,对于WPF来说,我的其余回答没有太多意义,而且由于我不是WPF方面的专家,所以无法确切地说。但是,有一种方法可以解决这个问题。如果您能改进另一个答案的内容,那么在SO上盗用其内容是完全合法的。因此,如果有人知道如何通过WPF usercontrol正确执行此操作,则可以提取整个答案的第一部分,并添加相关的WPF部分。

编辑:让我在此回应评论中的问题。

由于所涉及的类是用户控件,其生命周期将与窗体绑定。当窗体关闭时,它将处置所有拥有的子控件,换句话说,这里已经有一个Dispose方法

如果用户控件管理自己的事件,那么处理这个问题的正确方法是在Dispose方法中取消事件处理程序的挂钩。

(其余内容已删除)


感谢您对这两个问题的高质量回答。PPMM寿命更长。我应该在这些UserControls上实现IDisposable吗?过去,我曾遇到一些令人讨厌的问题,因为某些对象仍然由外部类事件拥有,这就是为什么我开始这样做的原因(我当时实现了IDisposable,但那些对象不是UserControls)。 - Martin Hennings
我似乎无法重写 Dispose() 方法。使用您的示例,我得到了“没有找到适合重写的方法”的错误提示。MyControl 是一个 WPF UserControl。这可能是原因吗? - Martin Hennings
啊,抱歉,那部分就没有意义了 :P - Lasse V. Karlsen
你在控件中使用了哪种基本类型? - Lasse V. Karlsen
这是一个WPF UserControl,继承自 System.Windows.Controls.UserControl。但是我找不到任何可以重写的 Dispose() 方法。 - Martin Hennings
2
在您的.xaml.cs文件中,只需继承自IDisposable。像这样:partial class MyUserControl : UserControl, IDisposable。然后您就可以实现一个dispose方法了。 - Phil

11

首先,我建议不要使用析构函数,而是使用Dispose()来清除你的资源。

其次,在我看来,如果这段代码位于一个经常创建且寿命较短的对象内部,最好自行 处理 移除事件处理程序,因为这会导致与该持有者对象的链接,则会 防止 GC 对其进行回收

敬礼。


这似乎是一个极具争议的话题。我的个人经验与你的答案相符。 - Esben Skov Pedersen

8

WPF不太支持IDisposable。如果您正在实现需要清理的WPF控件,您应该考虑连接到LoadedUnloaded事件中(或同时)。

也就是说,您在Loaded处理程序中连接到事件,在Unloaded处理程序中断开连接。当然,这只是一个选项,如果您的控件不需要在“未加载”时接收事件,并且可以正确地支持多个加载/卸载周期。

使用Loaded/Unloaded事件的优点是您不必手动处理用户控件在其使用的每个地方。但是,您应该注意,Unloaded事件在应用程序关闭后不会触发。例如,如果您的关闭模式为OnMainWindowClose,则其他窗口的Unloaded事件将不会触发。通常这不是问题。这只意味着您不能在应用程序终止之前/期间可靠地执行Unloaded中必须发生的操作。

请注意,当应用程序开始关闭时,不会引发Unloaded事件。应用程序关闭发生在ShutdownMode属性定义的条件发生时。如果您将清理代码放置在Unloaded事件的处理程序中,例如Window或UserControl,它可能无法按预期调用。 - Amen Jlili
https://learn.microsoft.com/en-us/dotnet/api/system.windows.frameworkelement.unloaded?view=netcore-3.1 - Amen Jlili
@AmenJlili 确实如此。但我已经写了“您应该知道,在应用程序关闭开始后,不会触发Unloaded事件。(...)”你是忽略了这一点还是想通过你的评论添加一些我漏掉的东西? - Paul Groke

4
如果代码执行到析构函数,那么就不再重要了。这是因为只有在不再监听任何事件时才会被销毁。如果它仍在监听事件,则不会被销毁。

2

如果PPMM是具有较长生命周期的外部对象,那么MyControl实例会被保持引用,这意味着实例永远不会被垃圾回收,也不会触发终结器,除非PPMM_FactorChanged是静态方法。

您不需要为了删除代码而保留ppmmEventHandler


你会建议改用 IDisposable 吗?(这是一个 WPF UserControl。) - Martin Hennings

2
GC会处理这个问题。虽然事件保持了强引用,但它仅在父对象本身上保持了强引用。最终,只有MyControl通过事件处理程序保持引用,因此GC将收集它。
另一方面,使用终结器(不是析构函数)是一种不好的做法。如果您想要取消注册事件,应考虑使用IDisposable。

1
有些情况下,在Finalizer/destructor中取消订阅事件可能会很有用,如果事件发布者保证取消订阅是线程安全的话。一个对象要取消订阅自己的事件是无用的,但是可以将一个公共对象持有对实际“完成所有工作”的私有对象的引用,并使该私有对象订阅事件,这是一种可行的模式。如果私有对象没有对公共对象的引用,则在没有人再对其感兴趣时,公共对象将变为可终结状态;其终结器将能够代表私有对象取消订阅。

不幸的是,只有在订阅事件的对象保证可以接受来自任何线程上下文的取消订阅请求时,此模式才能生效。虽然.NET要求“事件”契约的一部分是所有取消订阅方法必须是线程安全的,但出于某些原因,微软并没有强制执行这一要求。因此,即使Finalizer/destructor发现应尽快取消订阅事件,也没有标准机制可以实现它。


1

事件处理程序很棘手,很容易隐藏资源泄漏。正如Tigran所说的那样,使用IDisposable并忘记析构函数。我建议测量一下你是否做对了。仅通过在任务管理器中查看应用程序的内存消耗,就可以确定是否存在泄漏,如果你通过加载和关闭几千个窗口进行压力测试,这将会更加明显。


0

2) 这个确实有效

1) 我曾经遇到过这样的情况(使用应用内消息服务),事件处理程序绑定到全局对象上没有被释放,因此垃圾回收器无法回收该对象。我认为这通常是一种罕见的情况 - 如果您认为自己也遇到了这种情况,可以使用 Red Gate 的 ANTS 等分析工具轻松进行内存分析。


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