何时dispose方法不会被调用?

7
我最近阅读了这篇文章,并想知道为什么要将 Finalizer 和 Dispose 方法一起使用。我在这里了解到向 Finalizer 添加 Dispose 的原因。我的疑问是,在何种情况下会调用 Finalizer 而不是 Dispose 方法本身?是否有示例代码或者是基于软件运行的系统上发生的某些事情?如果是这样,什么情况下可能导致 GC 无法运行 Dispose 方法。
5个回答

11

这里的终结器的目的只是为了防止内存泄漏(如果您意外地未显式调用Dispose)。这也意味着,如果您希望在程序关闭时释放资源,您不必对对象进行处理,因为GC将被强制完成所有对象的终结和收集。

另一个相关的点是,在从终结器处处理对象时,需要略微不同的处理方式。

~MyClass()
{
    Dispose(false);
}

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

protected void Dispose(disposing)
{
    if (!this.disposed)
    {
        if (disposing)
        {
            // Dispose managed resources here.
        }
        // Dispose unmanaged resources here.
    }
    this.disposed = true;
}

您不要在终结器中处理托管资源的原因是这样做实际上会创建对它们的强引用,这可能会阻止GC有效地执行其任务并收集它们。当然,应始终显式关闭/处理非托管资源(例如Win32句柄等),因为CLR对它们没有了解。


1
在你的终结器中不要处理托管资源的另一个原因是,它们可能在终结器执行时已经被垃圾收集器回收了。试图在它们已经被收集后进行处理将导致运行时错误。 - LukeH
1
@Luke:没错,但是可以通过将所有引用设置为null,然后在处理之前进行空值检查来轻松避免这种情况。 - Noldorin
@Noldorin - 在你的例子中,注销事件应该放在哪里?我知道从技术上讲它们应该属于托管部分,但是如果我们有一些对象通过事件与这个类绑定,而我们没有在非托管部分注销它(假设用户没有直接调用Dispose并且将其留给GC清理),那么怎么办呢?把事件注销放在托管部分是否安全/可行,以确保发生这种情况?副作用可能是有人认为他们正在处理一个对象,但实际上由于这个类和其他类之间的事件链接,它永远不会被处理。 - SwDevMan81
@SwDevMan81:我理解你的担忧,但实际上事件与其他资源没有什么不同(它们实际上只是多路广播委托的包装器)。尽管如此,事件注销应该放在Dispose方法的托管块中。请考虑,如果是终结器调用Dispose方法,您可以保证事件将作为终结的一部分立即被GC注销。 - Noldorin
2
一个对象试图取消订阅它已经订阅的事件是没有用的。唯一的情况是当持有该订阅的对象变得可以进行垃圾回收时,终结器才会运行,此时该订阅将无效。 - supercat
当程序关闭时,由于GC将被强制完成并收集所有对象,因此GC不会在关闭时强制运行终结器。它通常会有时间运行终结器,但不能保证(如果程序异常终止,则显然不会发生)。 - Peter Duniho

5

这主要是为了保护你自己。你无法控制你的类的最终用户会做什么。通过提供一个 finalizer 以及 Dispose 方法,即使用户忘记调用 Dispose() 或者错误地使用你的类,GC 也会“Dispose”你的对象,适当地释放你的资源。


1
值得一提的是,垃圾回收器是非确定性的,因此不能保证你的终结器会在何时甚至是否被调用。 - LukeH
是的 - 如果您的程序运行足够长时间,您的对象最有可能被终结。此外,如果它干净地关闭,它也会被终结。但是,GC没有任何保证 - 这也是为什么IDisposable首先存在的部分原因。 - Reed Copsey

3

当对象被垃圾回收时,会调用Finalizer方法。但需要显式调用Dispose方法。在下面的代码中,将调用Finalizer方法,但不会调用Dispose方法。

class Foo : IDisposable
{
  public void Dispose()
  {
    Console.WriteLine("Disposed");
  }

  ~Foo()
  {
    Console.WriteLine("Finalized");
  }
}

...

public void Go()
{
  Foo foo = new Foo();
}

1
这并不完全正确。终结器会在对象符合垃圾回收条件之后的某个时间点被调用(即应用程序不再引用该实例)。然而,由于必须运行终结器以处理该实例,CLR 实际上会将对象作为根对象,并且直到终结器运行后才进行垃圾回收。 - Brian Rasmussen
同样没有任何保证一个对象将被垃圾回收,或者它的终结器将被调用。因此,确保正确地处理任何 IDisposable 对象是至关重要的。 - LukeH

2

必须显式调用Dispose()方法或将对象放在using语句中以调用Dispose()方法来释放dispose方法。GC总是会调用finalizer,因此如果有需要在对象被处理之前发生的事情,则finalizer至少应该检查一下对象中的所有内容是否已清理干净。

尽可能避免在finalizer中清理对象,因为与事先释放它们(如调用dispose)相比,这会导致额外的工作量,但您应该始终在finalizer中检查是否有需要删除的对象。


1
一个重要但很微妙的注意事项尚未提到:Dispose 的一个很少被考虑的目的是防止对象过早地被清理。带有终结器的对象必须小心编写,以免终结器在预期之前运行。终结器不能在最后一次方法调用开始之前运行(*), 但如果对象在方法完成后将被丢弃,它有时可能会在最后一次方法调用期间运行。正确 Dispose 对象的代码在调用 Dispose 之前不能放弃该对象,因此没有危险会对正确使用 Dispose 的代码造成破坏。另一方面,如果最后一个使用对象的方法使用了在终结器中将被清理的实体,垃圾回收器可能会调用对象的 Finalize 方法并清理仍在使用中的实体。解决方法是确保任何使用将由终结器清理的实体的调用方法必须在某个时间点之后跟随使用 "this" 的方法调用。GC.KeepAlive(this) 是一个很好的方法来实现这一点。

(*) 那些被扩展为不对对象执行任何操作的内联代码的非虚方法可能会免除此规则,但Dispose通常是或调用虚方法。


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