Dispose,它是什么时候被调用的?

43

考虑以下代码:

namespace DisposeTest
{
    using System;

    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Calling Test");

            Test();

            Console.WriteLine("Call to Test done");
        }

        static void Test()
        {
            DisposeImplementation di = new DisposeImplementation();
        }
    }

    internal class DisposeImplementation : IDisposable
    {
        ~DisposeImplementation()
        {
            Console.WriteLine("~ in DisposeImplementation instance called");
        }
        public void Dispose()
        {
            Console.WriteLine("Dispose in DisposeImplementation instance called");
        }
    }
}
Dispose方法从来没有被调用,即使我在Test()调用之后放置了一个等待循环。所以这相当糟糕。我想编写一个简单易用的类,以确保每个可能的资源都得到清理。我不想把这个责任交给我的类的用户。
可能的解决方案:使用using,或者自己调用Dispose(基本上是一样的)。我能强制用户使用using吗?或者我能强制调用Dispose吗?
在Test()之后调用GC.Collect()也不起作用。
将di设置为null也不会触发Dispose。析构函数确实有效,因此当对象退出Test()时,它会被析构。
好的,伙计们,现在清楚了!
谢谢大家的答案!我会在评论中添加一个警告!
7个回答

63

针对问题,需要指出几个重要的点:

  1. .NET GC 是非确定性的(即您永远不会知道,也不应该依赖于它何时发生)。
  2. Dispose 永远不会被 .NET Framework 调用;您必须手动调用它——最好是通过将其创建包装在 using() 块中来调用。
  3. 明确地将可处理的对象设置为 null 而未调用 Dispose() 是一件坏事。这意味着您明确地将对象的“根引用”设置为 null。这实际上意味着您无法稍后调用 Dispose,并且更重要的是,它将对象发送到 GC Finalization 队列以进行终结。应尽一切可能避免通过不良编程实践引起终结。
  4. 明确的是,并非 .NET 中的每个对象都需要通过 Dispose 模式“处理”;只有在使用实现了 IDisposableIDisposableAsync 的 .NET 类型时才需要处理 。

Finalizer: 一些开发人员将其称为析构函数。事实上,在C# 4.0 语言规范(第 1.6.7.6 节)ECMA-334 规范的以前版本中,它甚至被称为“析构函数”。幸运的是,第四版(2006 年 6 月)在第 8.7.9 节中正确地定义了 Finalizers,并在第 17.12 节中试图澄清两者之间的混淆。需要注意的是,在传统上所称的析构函数和 .NET Framework 中的析构函数/终结器之间存在重要的内部差异(这里不必详述)。

  1. 如果存在 Finalizer,则仅当未调用 GC.SuppressFinalize() 时,才会被 .NET Framework 调用。
  • 永远不应该显式调用finalizer。幸运的是,C#不会明确允许这样做(我不知道其他语言);尽管可以通过为GC的第二代调用GC.Collect(2)来强制进行。
  • 终结: 终结是.NET Framework处理'优雅'清理和释放资源的方式。

    1. 只有在终结队列中存在对象时才会发生。
    2. 只有在对Gen2进行垃圾回收时才会发生(对于良好编写的.NET应用程序,这大约是每100次回收1次)。
    3. 截至.NET 4,有一个单独的终结线程。如果此线程因任何原因被阻塞,您的应用程序将出现问题。
    4. 编写正确且安全的终结代码并不是微不足道的事情,很容易犯错误(即从Finalizer抛出异常、允许依赖其他可能已经完成终结的对象等)

    虽然这肯定比您要求的更多,但它提供了工作原理和为什么以这种方式工作的背景信息。有些人会争论他们不应该担心在.NET中管理内存和资源,但这并不改变必须完成这些任务的事实,我不认为这会在不久的将来消失。

    不幸的是,上面的示例(错误地)暗示您需要实现Finalizer作为标准Dispose模式的一部分。但是,除非您使用未托管的代码,否则不应该实现Finalizer。否则,会有负面的性能影响。

    我在此处发布了一个实现Dispose模式的模板:如何正确实现IDisposable模式?


    1
    “.NET Framework 永远不会调用 Dispose;您必须手动调用” - “using” 语句会在内部调用“Dispose”。 - James
    10
    “using”语句实际上是一个带有“finally”子句并在其中调用“Dispose”的try/finally语句。我的观点是,你必须实际上使用“using()”语句编写代码或自己调用“Dispose”。换句话说,如果你不在可处理资源的使用范围内使用“using()”语句,你可能会泄漏资源并/或将其放入Finalization队列中。 - Dave Black
    3
    我的意思是该评论对读者来说有点误导性,你可以更新为“*.NET Framework从不调用Dispose;您必须手动调用它或使用using语句包装可处理对象*”或类似的内容… - James
    1
    感谢您的澄清。我已经编辑了注释,包括使用“using()”块的内容。 - Dave Black
    在这篇文章中,他们同意大多数提出的想法。它可以作为一个参考来阅读更多关于这个问题的内容。https://www.codeproject.com/articles/29534/idisposable-what-your-mother-never-told-you-about - morhook

    28

    我想编写一个类,它既简单明了,易于使用,同时又能确保清理所有可能存在的资源。我不希望将这个责任放到我的类的用户身上。

    你做不到。内存管理系统只能处理专门用于内存的资源,而不能扩展到其他类型的资源。

    IDisposable模式是为开发人员准备的一种方式,通过这种方式告诉对象何时完成任务,而不是依靠像引用计数这样的内存管理方法去推测。

    你可以将终结器作为后备机制,用于处理未正确释放对象的用户,但它并不适合作为清理对象的主要方法。应该正确地释放对象,避免调用成本更高的终结器。


    4
    请注意,不要在您的终结器中调用任何其他对象。终结顺序未定义(特别是允许循环和其他交叉依赖关系),因此终结器只能清理非托管资源。 - Cygon
    9
    为什么要点踩?如果你不解释你认为哪里有问题,那么答案就无法得到改进。 - Guffa
    @Guffa - 只是为了明确起见,CLR 使用引用计数来跟踪分配。 - Dave Black
    1
    我认为说“毫无头绪的开发者”可能对那些提出问题的人有点苛刻。 - morhook
    @morhook 没错!我几周前从C++转到了C#,非常不满意用户必须显式调用清理资源。别叫我无知,有了智能指针,C++非常可靠且易于使用,而C#则需要一些手动的琐碎工作,我当然会忘记做某些事情。 - Yola
    显示剩余2条评论

    16

    所有答案(或多或少)都是正确的,这里是一个例子:

    static void Test()
    {
        using (DisposeImplementation di = new DisposeImplementation())
        {
            // Do stuff with di
        }
    }
    

    手动调用Dispose也可以起作用,但使用using语句的优点在于当控制块结束时对象也会被处理掉,即使因为抛出异常而离开控制块。

    如果有人“忘记”使用IDisposable接口,您可以添加一个最终器来处理资源释放:

    public class DisposeImplementation : IDisposable
    {    
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
    
        protected virtual void Dispose(bool disposing)
        {
            if (disposing)
            {
                // get rid of managed resources
            }   
            // get rid of unmanaged resources
        }
    
        ~DisposeImplementation()
        {
            Dispose(false);
        }
    }
    

    有关更多信息,请参见此问题。不过,这只是为那些没有正确使用您的类进行补偿 :) 我建议在终结器中添加一个大而肥的Debug.Fail()调用,以警告开发人员其错误。

    如果您选择实现该模式,您将看到GC.Collect()将触发处理。


    8

    使用此模板作为您的课程的样式/模板

    public class MyClass : IDisposable
    {
        private bool disposed = false;
    
        // Implement IDisposable.
        // Do not make this method virtual.
        // A derived class should not be able to override this method.
        public void Dispose()
        {
            Dispose(true);
            // This object will be cleaned up by the Dispose method.
            // Therefore, you should call GC.SupressFinalize to
            // take this object off the finalization queue
            // and prevent finalization code for this object
            // from executing a second time.
            GC.SuppressFinalize(this);
        }
    
        // Dispose(bool disposing) executes in two distinct scenarios.
        // If disposing equals true, the method has been called directly
        // or indirectly by a user's code. Managed and unmanaged resources
        // can be disposed.
        // If disposing equals false, the method has been called by the
        // runtime from inside the finalizer and you should not reference
        // other objects. Only unmanaged resources can be disposed.
        private void Dispose(bool disposing)
        {
            // Check to see if Dispose has already been called.
            if (!this.disposed)
            {
                // If disposing equals true, dispose all managed
                // and unmanaged resources.
                if (disposing)
                {
                    // Dispose managed resources.                
                    ......
                }
    
                // Call the appropriate methods to clean up
                // unmanaged resources here.
                // If disposing is false,
                // only the following code is executed.
                ...........................
    
                // Note disposing has been done.
                disposed = true;
            }
        }
    
        // Use C# destructor syntax for finalization code.
        // This destructor will run only if the Dispose method
        // does not get called.
        // It gives your base class the opportunity to finalize.
        // Do not provide destructors in types derived from this class.
        ~MyClass()
        {
            // Do not re-create Dispose clean-up code here.
            // Calling Dispose(false) is optimal in terms of
            // readability and maintainability.
            Dispose(false);
        }
    }
    

    当然,正如其他人提到的那样,请不要忘记 using(...){} 块。


    你忘了提到,除非你正在处理非托管资源,否则永远不要实现Finalizer。 - Dave Black
    1
    如果您不需要Finalizer,那么整个SuppressFinalize / disposing模式就是多余的。 - yoyo
    @yoyo - 绝对不是真的。仅仅因为你不需要一个 finalizer 并不意味着你不应该实现 IDisposable 接口。Finalizer(以及 Dispose 模式)对于清理/释放非托管资源是必要的。Dispose 模式/实现应该在任何时候需要清理托管资源时被实现。 - Dave Black
    // 不要在从这个类派生的类型中提供析构函数。 我认为如果它们还有需要处理的资源,那么可以提供析构函数。我阅读的方法是提供一个 ~DerivedClass() { Dispose(false); } 和一个 protected override void Dispose(bool disposing) { ... base.Dispose(disposing); }。 - FocusedWolf

    3

    您需要显式调用Dispose方法,或将对象包装在using语句中。例如:

    using (var di = new DisposeImplementation())
    {
    }
    

    可能的解决方案:使用using,或者调用Dispose自己(基本相同)。
    使用using与在finally块内调用Dispose是相同的。

    唯一不应使用“using()”语句的情况是在WCF客户端/代理的情况下。我在我的博客上发表了一篇文章,解释了原因,并提供了处理WCF客户端/代理的解决方案。http://dave-black.blogspot.com/2012/03/dont-use-using-with-wcf-proxy.html - Dave Black

    2

    在C#中并不存在析构函数,而是称之为finalizers。实际上你可以从finalizer中调用它(请参考我的回答),但是可能需要特殊的预防措施。 - Thorarin
    2
    C#确实有析构函数:请参阅语言规范或http://msdn.microsoft.com/en-us/library/66x5fx1b.aspx。 - Rodrick Chapman
    1
    @RodrickChapman:请参考Dave Black的答案。 它们使用与C ++析构函数相同的〜ClassName命名方式,但它们不是析构函数,而是终结器。 C#编译器获取所谓的“析构函数”,并使用它来实现Object.Finalize()的重载(它还会自动添加一些内容)。 - Ben Voigt

    1

    你应该自己处理它,可以调用Dispose方法或使用using语句。记住,这不是一个析构函数!

    如果你不能信任你的类的用户正确地处理资源,他们可能会在其他方面搞砸。


    “当人们试图设计完全防傻的东西时,常见的错误是低估了彻头彻尾的傻瓜的聪明才智。”
    • 道格拉斯·亚当斯,《几乎无害》
    - yoyo

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接