是否真的需要仅为托管资源实现dispose模式

问题描述 投票:3回答:4

我仔细阅读了this文章,它似乎清楚地表明应该在IDisposable实现的所有情况下实施处置模式。我试图理解为什么我需要在我的类只保存托管资源(即其他IDisposable成员或安全句柄)的情况下实现dispose模式。为什么我不能写

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();
    }
}

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

c# .net dispose idisposable finalizer
4个回答
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会阻止终结器尝试释放可能已经回收的托管资源。

访问已经回收的托管对象,或者在Disposed之后访问其属性(可能是由另一个终结器)将导致在Finalizer中引发异常,即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()但是GC可能已经回收了boo。


1
投票

我还没有看到提到的Dispose的这种特殊用法,所以我想在不使用dispose模式时我会指出一个常见的内存泄漏源。

Visual Studio 2017实际上是通过静态代码分析来抱怨我应该“实现配置模式”。请注意我正在使用SonarQube和SolarLint,我不相信Visual Studio会单独捕获它。 FxCop(另一种静态代码分析工具)可能会,尽管我没有测试过。

我注意到下面的代码展示了dispose模式也可以防止像这样的东西,它没有非托管资源:

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应该也可以取消订阅此事件。但它确实突出了事件订阅者超过发布者的常见情况。

我见过事件导致无数的内存泄漏。特别是在非常大或极端高性能的代码库中。

我几乎无法计算有多少次我看到物品在他们处置之后很久就存活了。这是一种非常常见的方式,许多分析器发现内存泄漏(处置的对象仍由某种GC根保持)。

再一次,过于简化的例子和可怕的代码。但是,在一个物体上调用Dispose并且不要指望它处理整个物体,无论它是否已经衍生出一百万次,实际上从来都不是好习惯。

编辑

请注意,这个答案是故意仅针对托管资源,展示了配置模式在这种情况下也很有用。这有目的地没有解决非托管资源的用例,因为我觉得缺乏对仅管理的用途的关注。在这里还有许多其他好的答案可以谈论这个问题。

但是,我会注意到一些对非托管资源很重要的快速事项。上面的代码可能无法解决非托管资源,但我想说清楚它与如何处理它们并不矛盾。

当你的班负责非托管资源时,使用finalizers非常重要。简而言之,垃圾收集器会自动调用终结器。所以它给你一个合理的保证,它总是在某个时间点被调用。它不是防弹的,但与希望用户代码调用Dispose相去甚远。

这种保证不适用于Dispose。在没有调用Dispose的情况下,GC可以回收一个对象。这是终结器用于非托管资源的关键原因。 GC本身只处理托管资源。

但我还要注意,同样重要的终结器不应该用于清理托管资源。有无数的原因(毕竟这是GC的工作),但使用终结器的最大缺点之一是延迟对象的垃圾收集。

GC看到一个对象可以自由回收但是有一个终结器,它会通过将对象放在终结器队列中来延迟收集。这会给对象增加不必要的生命周期,加上对GC的压力更大。

最后我会注意到终结器因为这个原因是非确定性的,尽管它具有与C ++中的析构函数类似的语法。他们是非常不同的野兽。您永远不应该依赖终结器来清理特定时间点的非托管资源。


0
投票

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

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

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

它在this文章中解释过,如果没有被压制,所有终结器都会被调用。如果被压制,一切都将由Diapose(true)电话链首先释放。


-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)会让这种奇怪的情况变得不可能。

© www.soinside.com 2019 - 2024. All rights reserved.