这些未订阅的事件内存泄漏什么时候发生?我应该编写析构函数还是实现 IDisposable 来取消订阅事件?
2 回答
假设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) { }
}
In this case any Foo instance will always outlive its composed Bar instance. When a Foo is unreachable, so will its Bar be. The subscribed event handler cannot keep the Foo alive here. The downside of this is that if Bar is a dependency in need of being mocked in unit testing scenarios, it can't (in any clean way) be explicitly instantiated by the consumer, but needs to be injected.
事件处理程序包含对声明处理程序的对象的强引用(在委托的Target
属性中)。
直到事件处理程序被删除(或直到拥有该事件的对象不再被任何地方引用),包含该处理程序的对象才会被收集。
您可以通过在不再需要处理程序时(可能在 中Dispose()
)删除处理程序来解决此问题。
终结器无济于事,因为终结器只有在它被收集后才会运行。