【问题标题】:Am I implementing IDisposable correctly?我是否正确实施 IDisposable?
【发布时间】:2009-07-16 08:33:49
【问题描述】:

此类使用StreamWriter,因此实现了IDisposable

public class Foo : IDisposable
{
    private StreamWriter _Writer;

    public Foo (String path)
    {
        // here happens something along the lines of:
        FileStream fileWrite = File.Open (path, FileMode.OpenOrCreate, FileAccess.Write, FileShare.ReadWrite);
        _Writer = new StreamWriter (fileWrite, new ASCIIEncoding ());
    }

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

    ~Foo()
    {
        Dispose (false);
    }

    protected virtual void Dispose (bool disposing)
    {
        if (_Disposed) {
            return;
        }
        if (disposing) {
            _Writer.Dispose ();
        }
        _Writer = null;
        _Disposed = true;
    }
    private bool _Disposed;
}

}

当前的实现有什么问题吗?即,我是否必须手动释放底层FileStreamDispose(bool)写对了吗?

【问题讨论】:

  • 在调用 Dispose 之前不要忘记检查并确保 _Writer 不为空。您的 Dispose 必须能够容忍多次调用而不会失败。
  • 其实他先检查_Disposed,所以没问题(虽然不是线程安全的)。
  • @mafutrct:有更新的答案。
  • 附加问题:_Dispoed = true 不应该只在disposing == true 时执行吗?

标签: c# idisposable


【解决方案1】:

如果您的类不直接使用非托管资源,则无需使用此扩展版本的 IDisposable 实现。

一个简单的

 public virtual void Dispose()
 {

     _Writer.Dispose();
 }

足够了。

如果您的消费者未能 Dispose 您的对象,它通常会在没有调用 Dispose 的情况下被 GC 处理,_Writer 持有的对象也将被 GC 处理,并且它会有一个终结器,因此它仍然可以清理其非托管资源。

编辑

在对 Matt 和其他人提供的链接进行了一些研究后,我得出的结论是,我的答案站得住。原因如下:-

可继承类上的一次性实现“模式”(我的意思是受保护的虚拟 Dispose(bool)、SuppressFinalize 等 marlarky)背后的前提是子类可能保留到非托管资源。

然而,在现实世界中,我们绝大多数 .NET 开发人员从不靠近非托管资源。如果你必须量化上面的“可能”,你会为你的 .NET 编码得出什么概率数字?

假设我有一个 Person 类型(为了论证,它的一个字段中有一个一次性类型,因此本身应该是一次性的)。现在我有继承者 Customer、Employee 等。如果有人继承 Person 并想要持有非托管资源,我是否真的可以用这个“模式”来混淆 Person 类?

有时,我们的开发人员可能会将事情过度复杂化,以尝试针对所有可能的情况编写代码,而无需使用关于此类情况的相对概率的一些常识。

如果我们想直接使用非托管资源,明智的模式将把这样的东西包装在它自己的类中,其中完整的“一次性模式”是合理的。因此,在大量“正常”代码中,我们不必担心所有这些乱七八糟的事情。如果我们需要 IDisposable,我们可以使用上面的简单模式,无论是否可继承。

唷,很高兴能从我的胸口中解脱出来。 ;)

【讨论】:

  • 这只有在类被密封的情况下才足够。
  • @Anthony,这是一个合理的反对意见,但我认为更多的复杂性来自于人们以不同的、不可预测的方式实现该模式。但你似乎知道自己在做什么,这是你的代码,由你决定。
  • @Matt:在 99% 的情况下,完全没有涉及到实际的直接非托管资源。因此,除非您确定确实需要这样做,否则我的建议不要这样做。
  • @Jordao:我同意“官方”模式相当糟糕,这是一个很好的发现——这是一个很好的选择!当然,缺点是人们比一些随机网站更了解“MS”的建议——即使他们更倾向于编写库的系统程序员而不是那些库的用户。
  • @Matt Howells:未来的维护者可能期望 MS 模式可能是使用它的好理由;恕我直言,这将是唯一的原因。 @Eamon Nerbonne:我不喜欢那种模式;我想不出一个可继承或继承的类应该直接持有非托管资源,除非基类的唯一原因是持有非托管资源(例如 SafeHandle),那么为什么要提供它呢?
【解决方案2】:

您不需要Finalize(析构函数)方法,因为您没有任何非托管对象。但是,您应该保留对GC.SuppressFinalize 的调用,以防从 Foo 继承的类具有非托管对象,因此是终结器。

如果您使类密封,那么您知道非托管对象永远不会进入等式,因此没有必要添加protected virtual Dispose(bool) 重载或GC.SuppressFinalize

编辑:

@AnthonyWJones 对此的反对意见是,如果您知道子类不会引用非托管对象,则整个 Dispose(bool)GC.SuppressFinalize 是不必要的。但如果是这种情况,你真的应该创建类internal 而不是public,并且Dispose() 方法应该是virtual。如果您知道自己在做什么,请随意不要遵循 Microsoft 建议的模式,但您应该在违反规则之前了解并理解规则!

【讨论】:

  • 这不是假设继承类正确实现了 Dispose 并且没有将非托管清理代码留在其终结器中吗?
  • 不,它没有做任何这样的假设,它只是允许继承类通过重写 Dispose(bool) 来正确实现处置。
  • 很抱歉,这对我来说没有任何意义。将代码包含在超类中以防子类执行 X 似乎完全违反了 OO 本质。当然,您会将其留给子类来处理非托管资源它使用、终结器和任何IDisposable 实现本身。它不应该假设 SuppressFinalize 将由基类调用。为什么要这样做?
  • @Anthony:因为它是 IDisposable.Dispose() 后置条件的一部分。派生类型负责在调用 Dispose(true) 时释放托管和非托管资源,如果它执行该职责(并且您假设它执行了),那么调用 SuppressFinalize 是安全的。
【解决方案3】:

建议的做法是仅当您拥有非托管资源(例如本机文件句柄、内存指针等)时才使用终结器。

我有两个小建议,

没有必要拥有 “m_Disposed”变量来测试是否 你之前打电话给Dispose 在您的托管资源上。你可以 使用类似的代码,

protected virtual void Dispose (bool disposing)
{
    if (disposing) {
        if (_Writer != null)
            _Writer.Dispose ();
    }
    _Writer = null;
}

仅在必要时打开文件。因此,在您的示例中,您将使用 File.Exists 在构造函数中测试文件是否存在,然后当您需要读取/写入文件时,然后您将打开它并使用它。

另外,如果您只是想将文本写入文件,请查看 File.WriteAllTextFile.OpenText 甚至 File.AppendText,它专门针对带有 ASCIIEncoding 的文本文件。

除此之外,是的,您正在正确实现 .NET Dispose 模式。

【讨论】:

  • 关于您帖子的第二部分:该文件必须始终保持打开状态。 Foo 实际上是一个记录器。 :)
  • @mafutrct: 始终打开文件并不是一个好习惯。在写入文件的函数中;打开文件,写入文件,然后在函数退出之前将其关闭。
【解决方案4】:

我有很多这样的课程 - 我的建议是尽可能密封课程。 IDisposable + 继承可以工作,但 99% 的时候你不需要它——而且很容易出错。

除非您正在编写公共 API(在这种情况下,最好允许人们按照他们的意愿实现您的代码 - 即使用 IDisposable),否则您不需要支持它。

只是做:

public sealed class Foo : IDisposable
{
    private StreamWriter _Writer;

    public Foo(string path)
    {
            FileStream fileWrite = File.Open (path, FileMode.OpenOrCreate, FileAccess.Write, FileShare.ReadWrite);
            try { 
                _Writer = new StreamWriter (fileWrite, new ASCIIEncoding());
            } catch {
                fileWrite.Dispose();
                throw;
            }
    }

    public void Dispose()
    {
         _Writer.Dispose();
    }
}

注意 try...catch;从技术上讲,StreamWriter 构造函数可能会抛出异常,在这种情况下,它永远不会获得 FileStream 的所有权,您必须自己处理它。

如果您确实使用了很多 IDisposables,请考虑将该代码放在 C++/CLI 中:这将为您生成所有样板代码(它透明地对本机和托管对象使用适当的确定性销毁技术)。

Wikipedia 有一个不错的 C++ IDisposable 示例(实际上,如果您有许多 IDisposable,C++ 实际上比 C# 简单得多): Wikipedia: C++/CLI Finalizers and automatic variables.

例如,在 C++/CLI 中实现一个“安全”的一次性容器看起来像......

public ref class MyDisposableContainer
{
    auto_handle<IDisposable> kidObj;
    auto_handle<IDisposable> kidObj2;
public:

    MyDisposableContainer(IDisposable^ a,IDisposable^ b) 
            : kidObj(a), kidObj2(b)
    {
        Console::WriteLine("look ma, no destructor!");
    }
};

即使不添加自定义 IDisposable 实现,此代码也将正确处理 KidObj 和 KidObj2,并且它对任何一个 Dispose 实现中的异常都很健壮(不是那些应该发生,但仍然存在),并且面对更多@ 987654329@ 会员或本地资源。

并不是说我是 C++/CLI 的忠实拥护者,但对于大量面向资源的代码,它可以轻松击败 C#,并且它与托管代码和本机代码具有绝对出色的互操作性 - 简而言之,完美的胶水代码;- )。我倾向于用 C# 编写 90% 的代码,但使用 C++/CLI 来满足所有互操作需求(尤其是如果您想调用任何 win32 函数 - MarshalAs 和其他地方的互操作属性令人恐惧且完全难以理解)。

【讨论】:

    【解决方案5】:

    在尝试处理它之前,您应该检查_Writer 不是null。 (看起来不太可能是null,但以防万一!)

    protected virtual void Dispose(bool disposing)
    {
        if (!_Disposed)
        {
            if (disposing && (_Writer != null))
            {
                _Writer.Dispose();
            }
            _Writer = null;
            _Disposed = true;
        }
    }
    

    【讨论】:

      【解决方案6】:

      如果你打开 StreamWriter,你也必须 Dispose 它,否则你会有泄漏。

      【讨论】:

      • 您的意思可能是 FileStream,但实际上释放 StreamWriter 确实也释放了 FileStream。
      • 谢谢,太糟糕了,我无法将您的评论作为答案的一部分。
      • 好吧,你不再有对 FileStream 的引用了......但正如 Martin 指出的那样,StreamWriter 将为你处理它。另外,由于 StreamWriter 实现了 IDisposable,所以你也必须 Dispose 。
      • 是的,当然,这就是 OP 代码中 Dispose 的用途。
      • 一个“打开”的 StreamWriter 对象中总是有一个内部 FileStream,无论它是使用您创建的文件初始化的,还是使用文件路径初始化的。
      【解决方案7】:

      我同意其他 cmets 中所说的一切,但也要指出这一点;

      1. 在任何一种情况下,您实际上都不需要设置 _Writer = null。

      2. 如果您要这样做,最好将其放在 dispose 所在的 if 中。它可能不会有太大的不同,但是当被终结器处理时,您通常不应该使用托管对象(正如其他人所指出的那样,在这种情况下无论如何您都不需要)。

      【讨论】:

        猜你喜欢
        • 2013-08-22
        • 1970-01-01
        • 2021-09-25
        • 1970-01-01
        • 2012-09-02
        • 1970-01-01
        • 1970-01-01
        • 2020-08-27
        • 2018-09-09
        相关资源
        最近更新 更多