是否真的只需要为托管资源实现处置模式?

时间:2019-04-16 16:00:50

标签: c# .net dispose idisposable finalizer

我已经仔细阅读了this的文章,并且似乎清楚地指出在所有IDisposable实现的情况下都应该实现处置模式。我试图了解为什么在我的班级仅包含托管资源(即其他IDisposable成员或安全句柄)的情况下需要实现处置模式的原因。为什么我不能写

class Foo : IDisposable
{
    IDisposable boo;

    void Dispose()
    {
        boo?.Dispose();
    }
}

如果可以肯定地知道不存在非托管资源,并且由于没有从终结器中释放托管资源,也没有必要从终结器中调用Dispose方法吗?

更新:为了增加清晰度。讨论似乎归结为以下问题:是否需要为每个实现IDisposable的基础公共非密封类实现处置模式。但是,当没有非托管资源的基类不使用dispose模式而具有非托管资源的子类确实使用此模式时,我找不到层次结构的潜在问题:

class Foo : IDisposable
{
    IDisposable boo;

    public virtual void Dispose()
    {
        boo?.Dispose();
    }
}

// child class which holds umanaged resources and implements dispose pattern
class Bar : Foo
{
    bool disposed;
    IntPtr unmanagedResource = IntPtr.Zero;

    ~Bar()
    {
        Dispose(false);
    }

    public override void Dispose()
    {
        base.Dispose();
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (disposed)
            return;

        if (disposing)
        {
            // Free any other managed objects here.
            //
        }
        // close handle

        disposed = true;
    }
}

// another child class which doesn't hold unmanaged resources and merely uses Dispose 
class Far : Foo
{
    private IDisposable anotherDisposable;

    public override void Dispose()
    {
        base.Dispose();
        anotherDisposable?.Dispose();
    }
}

更重要的是,对于我来说,当实现仅对他们所知道的事情负责时,看起来更好地分离了关注点。

4 个答案:

答案 0 :(得分:5)

private class Foo : IDisposable
{
    IDisposable boo;

    public void Dispose()
    {
        boo?.Dispose();
    }
}

很好。

public sealed class Foo : IDisposable
{
    IDisposable boo;

    public void Dispose()
    {
        boo?.Dispose();
    }
}
  

如果我没有使用虚拟Dispose方法以上述方式实现未密封的基类,怎么办?

来自docs

  

因为垃圾收集器销毁托管的顺序   在完成过程中未定义对象,调用此Dispose   值为false的重载阻止终结器尝试执行以下操作:   释放可能已经回收的托管资源。

访问已回收的托管对象,或在其被处置后访问其属性(可能是由另一个终结器),将导致在终结器中引发异常,bad

  

如果Finalize或Finalize的覆盖抛出异常,则   运行时不是由覆盖默认值的应用程序托管   策略,运行时终止进程,并且没有活动的try / finally   块或终结器被执行。此行为可确保流程   如果终结器无法释放或破坏资源,则为完整性。

所以,如果您有:

   public  class Foo : IDisposable
    {
        IDisposable boo;

        public virtual void Dispose()
        {
            boo?.Dispose();
        }
    }
    public class Bar : Foo
    {
        IntPtr unmanagedResource = IntPtr.Zero;
        ~Bar()
        {
            this.Dispose();
        }

        public override void Dispose()
        {
            CloseHandle(unmanagedResource);
            base.Dispose();
        }

        void CloseHandle(IntPtr ptr)
        {
            //whatever
        }
    }

〜Bar-> Bar.Dispose()-> base.Dispose()-> boo.Dispose()但是boo可能已被GC回收。

答案 1 :(得分:1)

我还没有看到Dispose的这种特殊用法,所以我想指出一个不使用处理方式的常见内存泄漏源。

Visual Studio 2017实际上是通过静态代码分析对此抱怨的,我应该“实现处置模式”。请注意,我正在使用SonarQube和SolarLint,而且我不认为Visual Studio会单独解决这一问题。 FxCop(另一种静态代码分析工具)可能会,尽管我没有对此进行测试。

我注意到下面的代码展示了处置模式,该代码也可以防止类似的情况,即没有不受管理的资源:

public class Foo : IDisposable
{
    IDisposable boo;

    public void Dispose()
    {
        boo?.Dispose();
    }
}

public class Bar : Foo
{
    //Memory leak possible here
    public event EventHandler SomeEvent;

    //Also bad code, but will compile
    public void Dispose()
    {
        someEvent = null;
        //Still bad code even with this line
        base.Dispose();
    }
}

以上说明了非常糟糕的代码。不要这样为什么这个可怕的代码?因为这个原因:

Foo foo = new Bar();
//Does NOT call Bar.Dispose()
foo.Dispose();

我们假设此可怕的代码已在我们的公共API中公开。考虑它的消费者使用的上述类:

public sealed class UsesFoo : IDisposable
{
    public Foo MyFoo { get; }

    public UsesFoo(Foo foo)
    {
        MyFoo = foo;
    }

    public void Dispose()
    {
        MyFoo?.Dispose();
    }
}

public static class UsesFooFactory
{
    public static UsesFoo Create()
    {
        var bar = new Bar();
        bar.SomeEvent += Bar_SomeEvent;
        return new UsesFoo(bar);
    }

    private static void Bar_SomeEvent(object sender, EventArgs e)
    {
        //Do stuff
    }
}

消费者完美吗?否。...UsesFooFactory也应该退订该活动。但这确实突出了一种常见的情况,即事件 subscriber 超出了 publisher

我已经看到事件导致无数内存泄漏。尤其是在超大型或超高性能代码库中。

我也很难数出我看到物体存活超过处置时间的次数。这是许多探查器发现内存泄漏(某些GC根仍然保留的处置对象)的一种非常常见的方式。

再次,过于简化的示例和可怕的代码。但是,对对象调用Dispose期望它处置整个对象(无论是否源自一百万次)都不是一种好习惯。

修改

请注意,此答案是有意仅解决托管资源的问题,这表明处置模式在这种情况下也很有用。这有目的地不解决非托管资源的用例,因为我觉得缺乏对仅托管用途的关注。而且,这里还有很多其他好的答案。

但是,我将注意一些有关非托管资源的重要提示。上面的代码可能无法解决非托管资源,但是我想表明它与应该应如何处理不矛盾。

当类负责非托管资源时,使用finalizers非常重要。简短地说,垃圾收集器会自动调用终结器。因此,它为您提供合理的保证,使其始终在某个时间点被调用。它不是防弹的,但是与希望用户代码调用Dispose相去甚远。

对于Dispose,此保证是 not 正确的。 GC可以回收对象,而无需调用Dispose。这就是终结器用于非托管资源的关键原因。 GC本身仅处理托管资源。

但是我还要指出,不应用于清理托管资源同样重要。为什么有很多原因(毕竟这是GC的工作),但是使用终结器的最大缺点之一就是延迟了对象的垃圾回收。

GC看到对象可以自由回收但具有终结器,它将通过将对象放置在终结器队列中来延迟收集。这样会增加对象不必要的使用寿命,并对GC造成更大压力。

最后,我要指出的是,尽管具有与C ++中的析构函数类似的语法,但由于此原因,终结器是不确定的。它们是非常不同的野兽。您绝对不应在特定时间依赖终结器来清理非托管资源。

答案 2 :(得分:0)

您可能理解错了。如果您没有非托管资源,则无需实施finilizer。您可以通过在Visual Studio中使用自动模式实现来检查它(它甚至会生成注释,说只有在使用非托管资源的情况下,才应该取消注释终结器)。

  

处置模式仅用于访问非托管资源的对象。

如果您设计基类,并且某些继承类访问非托管资源,那么继承类将通过覆盖Dispose(bool)并定义终结器来自行处理。

this文章对此进行了解释,如果不取消抑制,则将最终调用所有终结器。而且,如果抑制了一切,那么最初的Diapose(true)调用链会释放所有内容。

答案 3 :(得分:-1)

如果Dispose()是使用公共虚拟方法实现的,则希望重写该方法的派生类可以这样做,并且一切都会很好。但是,如果继承链上的任何内容通过重写公共虚拟IDisposable.Dispose()方法之外的方法来实现Dispose(),则可能导致子派生类无法实现自己的IDisposable.Dispose()同时仍然能够访问父级实现。

可以使用Dispose(bool)模式,而不管是否有公共Dispose()方法,因此避免了在类公开或不公开类的情况下需要使用单独的模式公用Dispose()方法。 GC.SuppressFinalize(this)通常可以用GC.KeepAlive(this)代替,但是对于没有终结器的类,其成本大致相同。如果没有该调用,则在类自己的Dispose方法运行时,可以触发类持有引用的任何对象的终结器。不太可能出现这种情况,即使发生也不会导致问题的通常情况,但是将this传递给GC.KeepAlive(Object)GC.SuppressFinalize(Object)使得这种古怪的情况成为不可能。