C#事件内存泄漏

C#事件内存泄漏,c#,memory-leaks,C#,Memory Leaks,这些未订阅事件何时发生内存泄漏?我应该编写析构函数还是实现IDisposable以取消订阅事件?事件处理程序包含对声明处理程序的对象的强引用(在委托的目标属性中) 在移除事件处理程序之前(或者在拥有事件的对象不再在任何地方被引用之前),将不会收集包含该处理程序的对象 当不再需要处理程序时(可能在Dispose()中),可以通过删除处理程序来解决此问题。 终结器帮不上忙,因为终结器只有在可以收集它之后才会运行。假设A引用B。此外,假设您认为您已经处理完B,并希望它被垃圾收集 现在,如果A是可访问的

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

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

在移除事件处理程序之前(或者在拥有事件的对象不再在任何地方被引用之前),将不会收集包含该处理程序的对象

当不再需要处理程序时(可能在
Dispose()
中),可以通过删除处理程序来解决此问题。

终结器帮不上忙,因为终结器只有在可以收集它之后才会运行。

假设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无法访问时,它的条也将无法访问。订阅的事件处理程序无法在此处使Foo保持活动状态。这样做的缺点是,如果Bar是一个依赖项,需要在单元测试场景中进行模拟,那么它不能(以任何干净的方式)由使用者显式实例化,而是需要注入

[1]


[2]

简单规则:不要使用终结器。因此,我不应该为了安全起见手动删除所有事件处理程序吗?这取决于声明事件的对象是否及时收集。静态事件不是这种情况,因此您应该按照描述的方式注销处理程序。@fex:您应该抽出时间来计算每个对象将持续多长时间,并在必要时删除。如果你想产生一个可靠的程序,了解它的工作原理是非常重要的。如果无法访问引用对象,你难道没有一个循环引用,它可以使两个对象都保持活动状态吗?Foo有一个对_-bar的引用,而_-bar通过它的处理程序有一个对该Foo的引用。我认为这是循环引用是错误的吗?C#GC是否足够聪明,能够处理循环引用和垃圾收集?还是我们真的发现自己仍然存在内存泄漏?循环引用不会影响垃圾收集。原因在这里有很好的描述: