C# 事件内存泄漏

16

这些未订阅事件的内存泄漏会在何时发生?我应该编写析构函数还是实现IDisposable接口以取消订阅事件?


5
简单规则:不要使用终结器。 - SLaks
2个回答

37

假设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是需要在单元测试场景中模拟的依赖项,则不能(以任何干净的方式)由使用者显式地实例化,但需要注入。
[1] http://msdn.microsoft.com/en-us/magazine/bb985010.aspx [2] http://en.wikipedia.org/wiki/Memory_leak

如果引用对象不可访问,那么难道不会出现循环引用,从而使两个对象都保持活动状态吗?Foo 对象具有对 _bar 的引用,而 _bar 通过其处理程序对该 Foo 对象具有引用。我认为这是一个循环引用,我的想法是否正确?C# GC 是否足够智能以处理循环引用并进行垃圾回收?还是我们实际上仍然存在内存泄漏问题? - Didier A.
2
循环引用不会影响垃圾回收。原因在这里得到了完美的描述:https://dev59.com/RnRC5IYBdhLWcg3wJNcN#400727 - Johann Gerell

6

事件处理程序包含对声明处理程序的对象的强引用(在委托的Target属性中)。

除非删除事件处理程序(或者不再引用拥有事件的对象),包含处理程序的对象将不会被收集。

当您不再需要它时(例如在Dispose()中),可以通过删除处理程序来解决此问题。
最终器无法帮助,因为最终器只会在可以收集时运行。


那我难道不应该为了安全起见手动删除所有事件处理程序吗? - fex
1
取决于声明事件的对象是否及时被收集。对于静态事件,情况并非如此,因此您应按照所述的方式取消注册处理程序。 - Sebastian Graf
2
@fex:你应该抽出时间来弄清楚每个对象的寿命,并在必要时进行删除。如果你想要编写一个可靠的程序,理解它应该如何工作非常重要。 - SLaks

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