27

有两种方法(我知道)在 C# 中导致无意的内存泄漏:

  1. 不处置实施的资源IDisposable
  2. 不正确地引用和取消引用事件。

我真的不明白第二点。如果源对象的生命周期比侦听器长,并且在没有其他对它的引用时侦听器不再需要事件,则使用正常的 .NET 事件会导致内存泄漏:源对象将侦听器对象保存在内存中应该被垃圾收集。

你能解释一下事件如何用 C# 中的代码导致内存泄漏,以及我如何使用弱引用和不使用弱引用来编写代码来解决它?

4

3 回答 3

37

当侦听器将事件侦听器附加到事件时,源对象将获得对侦听器对象的引用。这意味着在分离事件处理程序或收集源对象之前,垃圾收集器无法收集侦听器。

考虑以下类:

class Source
{
    public event EventHandler SomeEvent;
}

class Listener
{
    public Listener(Source source)
    {
        // attach an event listner; this adds a reference to the
        // source_SomeEvent method in this instance to the invocation list
        // of SomeEvent in source
        source.SomeEvent += new EventHandler(source_SomeEvent);
    }

    void source_SomeEvent(object sender, EventArgs e)
    {
        // whatever
    }
}

...然后是以下代码:

Source newSource = new Source();
Listener listener = new Listener(newSource);
listener = null;

即使我们分配nulllistener,它也不符合垃圾回收条件,因为newSource它仍然持有对事件处理程序 ( Listener.source_SomeEvent) 的引用。要修复这种泄漏,重要的是在不再需要事件侦听器时始终分离它们。

编写上面的示例是为了关注泄漏问题。为了修复该代码,最简单的方法可能是Listener保留对 的引用Source,以便以后可以分离事件侦听器:

class Listener
{
    private Source _source;
    public Listener(Source source)
    {
        _source = source;
        // attach an event listner; this adds a reference to the
        // source_SomeEvent method in this instance to the invocation list
        // of SomeEvent in source
        _source.SomeEvent += source_SomeEvent;
    }

    void source_SomeEvent(object sender, EventArgs e)
    {
        // whatever
    }

    public void Close()
    {
        if (_source != null)
        {
            // detach event handler
            _source.SomeEvent -= source_SomeEvent;
            _source = null;
        }
    }
}

然后调用代码可以发出信号表明它已使用该对象完成,这将删除Source必须对“Listener”的引用;

Source newSource = new Source();
Listener listener = new Listener(newSource);
// use listener
listener.Close();
listener = null;
于 2010-09-07T21:21:49.787 回答
13

阅读 Jon Skeet关于事件的优秀文章。这不是经典意义上的真正的“内存泄漏”,而更多的是尚未断开连接的持有引用。-=因此,请始终向事件处理程序记住,您+=在前一点应该是金子。

于 2010-09-07T21:57:44.970 回答
2

严格来说,托管 .NET 项目的“沙箱”中不存在“内存泄漏”;只有引用的保存时间超过了开发人员认为必要的时间。Fredrik 有此权利;当您将处理程序附加到事件时,因为处理程序通常是实例方法(需要实例),所以只要维护此引用,包含侦听器的类的实例就会保留在内存中。如果侦听器实例依次包含对其他类的引用(例如,对包含对象的反向引用),则在侦听器超出所有其他范围后,堆可能会保持相当长的时间。

也许对 Delegate 和 MulticastDelegate 有更深奥知识的人可以对此有所了解。在我看来,如果以下所有条件都属实,则可能发生真正的泄漏:

  • 事件侦听器需要通过实现 IDisposable 来释放外部/非托管资源,但它要么不需要,要么
  • 事件多播委托不会从其重写的 Finalize() 方法调用 Dispose() 方法,并且
  • 包含该事件的类不会通过其自己的 IDisposable 实现或在 Finalize() 中调用委托的每个 Target 上的 Dispose()。

我从来没有听说过任何涉及在委托目标上调用 Dispose() 的最佳实践,更不用说事件侦听器了,所以我只能假设 .NET 开发人员知道他们在这种情况下在做什么。如果这是真的,并且事件背后的 MulticastDelegate 尝试正确处置侦听器,那么只需在需要处置的侦听类上正确实现 IDisposable。

于 2010-09-07T21:51:11.793 回答