141

.NET IDisposable 模式 意味着,如果您编写终结器并实现 IDisposable,则终结器需要显式调用 Dispose。这是合乎逻辑的,也是我在极少数需要终结器的情况下一直做的事情。

但是,如果我这样做会发生什么:

class Foo : IDisposable
{
     public void Dispose(){ CloseSomeHandle(); }
}

并且不要实现终结器或任何东西。框架会为我调用 Dispose 方法吗?

是的,我意识到这听起来很愚蠢,所有逻辑都暗示它不会,但我的脑后总是有两件事让我不确定。

  1. 几年前有人告诉我,它实际上会这样做,而且那个人在“了解他们的东西”方面有着非常可靠的记录。

  2. 编译器/框架会根据您实现的接口(例如:foreach、扩展方法、基于属性的序列化等)执行其他“魔术”操作,因此这也可能是“魔术”是有道理的。

虽然我已经阅读了很多关于它的东西,并且暗示了很多东西,但我从来没有能够找到一个明确的“是”或“否”的答案来回答这个问题。

4

9 回答 9

127

.Net 垃圾收集器在垃圾收集上调用对象的 Object.Finalize 方法。默认情况下,它什么都不做,如果你想释放额外的资源,必须覆盖它。

Dispose 不会自动调用,如果要释放资源,则必须显式调用,例如在“使用”或“尝试最终”块中

有关详细信息,请参阅http://msdn.microsoft.com/en-us/library/system.object.finalize.aspx

于 2008-09-05T00:40:06.167 回答
71

我想在他的评论中强调布赖恩的观点,因为这很重要。

终结器不是 C++ 中的确定性析构函数。正如其他人所指出的那样,无法保证何时调用它,实际上,如果你有足够的内存,它是否会调用。

但是终结器的坏处是,正如 Brian 所说,它会导致您的对象在垃圾收集中幸存下来。这可能很糟糕。为什么?

您可能知道也可能不知道,GC 分为几代——Gen 0、1 和 2,以及大对象堆。拆分是一个松散的术语——你得到一个内存块,但是有第 0 代对象开始和结束的指针。

思考的过程是,您可能会使用许多寿命很短的对象。所以这些对于 GC 来说应该是容易和快速的 - Gen 0 对象。所以当有内存压力时,它做的第一件事就是 Gen 0 集合。

现在,如果这不能解决足够的压力,那么它会返回并执行第 1 代扫描(重做第 0 代),然后如果仍然不够,它会执行第 2 代扫描(重做第 1 代和第 0 代)。因此清理长寿命的对象可能需要一段时间并且相当昂贵(因为您的线程可能在操作期间被挂起)。

这意味着如果您执行以下操作:

~MyClass() { }

无论如何,您的对象都将存活到第 2 代。这是因为 GC 在垃圾回收期间无法调用终结器。因此,必须完成的对象被移动到一个特殊的队列中,由不同的线程清除(终结器线程 - 如果你杀死它,就会发生各种坏事)。这意味着您的对象会停留更长时间,并可能会强制进行更多的垃圾回收。

所以,所有这些只是为了让你明白你想尽可能使用 IDisposable 清理资源并认真尝试找到使用终结器的方法。这符合您的应用程序的最佳利益。

于 2008-09-05T01:52:46.850 回答
34

这里已经有很多很好的讨论,我参加聚会有点晚了,但我想自己补充几点。

  • 垃圾收集器永远不会直接为您执行 Dispose 方法。
  • GC在需要时执行终结器。
  • 用于具有终结器的对象的一种常见模式是让它调用一个按照约定定义为 Dispose(bool disposing) 的方法,传递 false 以指示调用是由于终结而不是显式的 Dispose 调用。
  • 这是因为在完成对象时对其他托管对象做出任何假设是不安全的(它们可能已经完成)。

class SomeObject : IDisposable {
 IntPtr _SomeNativeHandle;
 FileStream _SomeFileStream;

 // Something useful here

 ~ SomeObject() {
  Dispose(false);
 }

 public void Dispose() {
  Dispose(true);
 }

 protected virtual void Dispose(bool disposing) {
  if(disposing) {
   GC.SuppressFinalize(this);
   //Because the object was explicitly disposed, there will be no need to 
   //run the finalizer.  Suppressing it reduces pressure on the GC

   //The managed reference to an IDisposable is disposed only if the 
   _SomeFileStream.Dispose();
  }

  //Regardless, clean up the native handle ourselves.  Because it is simple a member
  // of the current instance, the GC can't have done anything to it, 
  // and this is the onlyplace to safely clean up

  if(IntPtr.Zero != _SomeNativeHandle) {
   NativeMethods.CloseHandle(_SomeNativeHandle);
   _SomeNativeHandle = IntPtr.Zero;
  }
 }
}

这是简单的版本,但是有很多细微差别会让你在这种模式上绊倒。

  • IDisposable.Dispose 的约定表明多次调用必须是安全的(对已释放的对象调用 Dispose 应该什么都不做)
  • 正确管理一次性对象的继承层次结构可能会变得非常复杂,尤其是在不同层引入新的一次性和非托管资源时。在上面的模式中,Dispose(bool) 是虚拟的,允许对其进行覆盖以便对其进行管理,但我发现它容易出错。

在我看来,最好完全避免使用任何直接包含一次性引用和可能需要最终确定的本机资源的类型。SafeHandles 提供了一种非常简洁的方法,将本机资源封装到内部提供自己的终结处理(以及许多其他好处,例如在 P/Invoke 期间删除窗口,其中本机句柄可能由于异步异常而丢失) .

简单地定义一个 SafeHandle 使这变得微不足道:


private class SomeSafeHandle
 : SafeHandleZeroOrMinusOneIsInvalid {
 public SomeSafeHandle()
  : base(true)
  { }

 protected override bool ReleaseHandle()
 { return NativeMethods.CloseHandle(handle); }
}

允许您将包含类型简化为:


class SomeObject : IDisposable {
 SomeSafeHandle _SomeSafeHandle;
 FileStream _SomeFileStream;
 // Something useful here
 public virtual void Dispose() {
  _SomeSafeHandle.Dispose();
  _SomeFileStream.Dispose();
 }
}
于 2008-09-05T11:07:14.940 回答
6

我不这么认为。您可以控制何时调用 Dispose,这意味着理论上您可以编写对(例如)其他对象的存在进行假设的处置代码。您无法控制何时调用终结器,因此让终结器自动代表您调用 Dispose 是很困难的。


编辑:我离开并进行了测试,以确保:

class Program
{
    static void Main(string[] args)
    {
        Fred f = new Fred();
        f = null;
        GC.Collect();
        GC.WaitForPendingFinalizers();
        Console.WriteLine("Fred's gone, and he's not coming back...");
        Console.ReadLine();
    }
}

class Fred : IDisposable
{
    ~Fred()
    {
        Console.WriteLine("Being finalized");
    }

    void IDisposable.Dispose()
    {
        Console.WriteLine("Being Disposed");
    }
}
于 2008-09-05T00:37:22.803 回答
4

不是你描述的情况,但是如果你有一个,GC 会为你调用终结器。

然而。下一次垃圾回收,对象不会被回收,而是进入终结队列,所有东西都被回收,然后调用终结器。之后的下一个集合将被释放。

根据您的应用程序的内存压力,您可能有一段时间没有用于该对象生成的 gc。因此,在文件流或数据库连接的情况下,您可能需要等待一段时间才能在终结器调用中释放非托管资源一段时间,从而导致一些问题。

于 2008-09-05T00:40:04.480 回答
1

不,它没有被调用。

但这很容易不要忘记处理您的对象。只需使用using关键字。

我为此做了以下测试:

class Program
{
    static void Main(string[] args)
    {
        Foo foo = new Foo();
        foo = null;
        Console.WriteLine("foo is null");
        GC.Collect();
        Console.WriteLine("GC Called");
        Console.ReadLine();
    }
}

class Foo : IDisposable
{
    public void Dispose()
    {

        Console.WriteLine("Disposed!");
    }
于 2008-09-05T00:40:12.763 回答
1

GC不会调用 dispose。它可能会调用您的终结器,但即使在所有情况下都不能保证。

有关处理此问题的最佳方法的讨论,请参阅本文。

于 2008-09-05T00:42:47.767 回答
0

IDisposable上的文档对行为以及示例代码给出了非常清晰和详细的解释。GC 不会调用Dispose()接口上的方法,但会调用对象的终结器。

于 2008-09-05T00:47:07.600 回答
0

IDisposable 模式的创建主要是为了由开发人员调用,如果您有一个实现 IDispose 的对象,开发人员应该using围绕对象的上下文实现关键字或直接调用 Dispose 方法。

该模式的故障保险是实现调用 Dispose() 方法的终结器。如果您不这样做,您可能会造成一些内存泄漏,即:如果您创建一些 COM 包装器并且从不调用 System.Runtime.Interop.Marshall.ReleaseComObject(comObject) (将放置在 Dispose 方法中)。

除了跟踪包含终结器的对象并由 GC 将它们存储在终结器表中并在 GC 启动一些清理启发式方法时调用它们之外,clr 中没有任何魔法可以自动调用 Dispose 方法。

于 2008-09-05T07:31:24.117 回答