这些未订阅事件的内存泄漏会在何时发生?我应该编写析构函数还是实现IDisposable接口以取消订阅事件?
这些未订阅事件的内存泄漏会在何时发生?我应该编写析构函数还是实现IDisposable接口以取消订阅事件?
假设A引用了B。进一步地,如果你认为你已经完成了对B的使用并希望其被垃圾回收。
现在,如果A是可达的[1],尽管你“完成了对它的使用”,B将不会被垃圾回收。这实质上是一种内存泄漏[2]。
如果B订阅了A中的事件,则我们面临相同的情况:通过事件处理程序委托,A引用了B。
那么,什么时候会出现问题?只有在引用对象可达的情况下才会出现问题,如上所述。在这种情况下,当不再使用Foo实例时,就可能会出现泄漏:
class Foo
{
Bar _bar;
public Foo(Bar bar)
{
_bar = bar;
_bar.Changed += BarChanged;
}
void BarChanged(object sender, EventArgs e) { }
}
可能会出现内存泄漏的原因是传递给构造函数的 Bar 实例的生命周期可能比使用它的 Foo 实例更长。订阅事件处理程序可以使 Foo 实例一直存在。
在这种情况下,需要提供一种方法来取消事件订阅,以避免内存泄漏。其中一种方法是让 Foo 实现 IDisposable 接口。这样做的好处是清楚地向类的消费者表示在完成后需要调用 Dispose() 方法。另一种方法是有单独的 Subscribe() 和 Unsubscribe() 方法,但这并不能传达类型的期望 - 它们太可选了,会引入时间耦合。
我的建议是:
class sealed Foo : IDisposable
{
readonly Bar _bar;
bool _disposed;
...
public void Dispose()
{
if (!_disposed)
{
_disposed = true;
_bar.Changed -= BarChanged;
}
}
...
}
或者另一种选择:
class sealed Foo : IDisposable
{
Bar _bar;
...
public void Dispose()
{
if (_bar != null)
{
_bar.Changed -= BarChanged;
_bar = null;
}
}
...
}
另一方面,当引用对象不可访问时,就不可能发生泄漏:
class sealed Foo
{
Bar _bar;
public Foo()
{
_bar = new Bar();
_bar.Changed += BarChanged;
}
void BarChanged(object sender, EventArgs e) { }
}
在这种情况下,任何一个Foo实例都将始终存在于其组成的Bar实例之外。当Foo不可达时,Bar也会随之而去。在此,订阅事件处理程序无法使Foo保持活动状态。这样做的缺点是,如果Bar是需要在单元测试场景中模拟的依赖项,则不能(以任何干净的方式)由使用者显式地实例化,但需要注入。事件处理程序包含对声明处理程序的对象的强引用(在委托的Target
属性中)。
除非删除事件处理程序(或者不再引用拥有事件的对象),包含处理程序的对象将不会被收集。
当您不再需要它时(例如在Dispose()
中),可以通过删除处理程序来解决此问题。
最终器无法帮助,因为最终器只会在可以收集时运行。