C#中关于Dispose()和析构函数的两个问题

16

我有一个关于如何使用Dispose()和析构函数的问题。阅读了一些文章和MSDN文档,这似乎是实现Dispose()和析构函数的推荐方式。

但是,我有两个关于这种实现方式的问题,可以在下面阅读到:

class Testing : IDisposable
{
    bool _disposed = false;

    protected virtual void Dispose(bool disposing)
    {
        if (!_disposed) // only dispose once!
        {
            if (disposing)
            {
                // Not in destructor, OK to reference other objects
            }
            // perform cleanup for this object
        }
        _disposed = true;
    }

    public void Dispose()
    {
        Dispose(true);

        // tell the GC not to finalize
        GC.SuppressFinalize(this);
    }

    ~Testing()
    {
        Dispose(false);
    }
}

在Dispose()中调用GC.SuppressFinalize(this)

当程序员使用 using 或显式调用 Dispose() 时,我们的类会调用 GC.SuppressFinalize(this)。我的问题是:

  • 这到底意味着什么? 对象会被收集但不会调用析构函数吗?我猜答案是肯定的,因为析构函数会被框架转换为 Finalize() 调用,但我不确定。

没有调用Dispose()时的Finalize()

假设 GC 将清理我们的对象,但程序员没有调用 Dispose()

  • 为什么此时我们不能释放资源?换句话说,为什么我们不能在析构函数中释放资源?
  • if 中必须执行什么代码?什么代码应该在 if 外面执行?

if (!_disposed) // only dispose once!
{
   if (disposing)
   {
       //What should I do here and why?
   }
   // And what here and why?
}

提前感谢。


据我所知,这种模式已经过时了。对于非托管资源,通常应该使用 SafeHandle,而托管资源通常不需要终结器。 - CodesInChaos
@codein,你说得对,但是MSDN上有规范的文档吗? - H H
@CodeInChaos:你能提供一些相关文献吗? - Daniel Peñalba
2
个人而言,我赞同这种观点:(非关键的)终结器应该做的唯一事情就是通知程序员可能存在的错误。 - CodesInChaos
2
我手头没有官方参考资料,但有几篇博客描述了普通终结的问题(特别是它不总是执行并且很难编码)或者safehandles/critical finalization的优点。http://blogs.msdn.com/b/bclteam/archive/2005/03/15/396335.aspx http://blogs.msdn.com/b/cbrumme/archive/2004/02/20/77460.aspx http://msdn.microsoft.com/en-us/library/fh21e17c(v=vs.90).aspx - CodesInChaos
@HenkHolterman,这个不在MSDN上,但是它是由Stephen Cleary编写的"IDisposable:关于资源释放你母亲从未告诉过你的事情"。 - Scott Chamberlain
3个回答

13

1. SuppressFinalize是什么作用?

它会将对象从终结器列表中注销,也就是说当GC回收对象时,会忽略析构函数的存在。这样做可以大大提高性能,因为否则析构函数需要延迟对象及其所有引用的收集。

2. 为什么我们不能在析构函数中释放[托管]资源?

虽然你可以这样做,但这肯定是无意义的:你所处的对象已经无法被访问,所以所有所拥有的托管资源也是无法访问的。它们将在同一运行中由GC进行终结和回收,调用Dispose()是不必要的,但并非没有风险或成本。

2a 在if内部和外部分别执行哪些代码?

if(disposing)内部,调用_myField.Dispose()

换句话说,处理托管资源(具有Dispose方法的对象)

在外部,调用清除(关闭)非托管资源的代码,例如Win32API.Close(_myHandle)

请注意,如果没有非托管资源(通常情况下都是如此,查找SafeHandle),那么就不需要析构函数,因此也不需要使用SuppressFinalize。

这使得完整(官方)的实现模式只在Test被继承的可能性下才是必需的。
请注意,Dispose(bool)是受保护的。当你声明你的类Testing为sealed时,完全可以省略~Testing(), 这样做是安全和合规的。


@Henk:在您关于2a的编辑之后,这意味着即使程序员忘记调用dispose,非托管资源也会被清理干净了吗? - Daniel Peñalba
2
@Guffa:只要你持有它,引用就不会失效。类型安全一直持续到最终阶段。这只是有点棘手:myField可能已经被Dispose(但尚未收集!)。在Disposed对象上Dispose()应该是安全的。 - H H
1
@Guffa:我认为Henk是正确的,因为他的意思是在disposing块内处理托管对象,而这不是终结器,而是程序员调用的Dispose()方法。 - Daniel Peñalba
1
Henk是对的,但是值得再次强调的是,在您的终结器中引用托管的IDisposable资源可能会使其比必要的时间更长。 如果您没有在终结器中引用它,则GC将可以自由地收集它(并调用其回退终结器(如果有)),只要不再需要它即可。 您无需在终结器中处置托管的IDisposable资源:如果这些资源需要任何后备终结处理,则应在其内部自行处理。 - LukeH
@Guffa,没错,你必须回到问题上了解这是特指“在此时刻”关于托管资源的。 - H H
显示剩余8条评论

2

第一部分:

当调用GC.SuppressFinalize(this)时,GC被告知该对象已经释放了其资源,它可以像任何其他对象一样进行垃圾回收。是的,在.NET中,finalization和“析构函数”是相同的东西。

第二部分:

finalization由单独的线程完成,我们无法控制finalization的时间和顺序,因此我们不知道是否还有其他对象可用或已经进行了最终处理。因此,您不能在disposing块之外引用其他对象。


请注意,当系统确定某些对象符合终结条件时,所有这些对象以及它们直接或间接引用的所有对象将变为“活动状态”,直到终结器实际运行。如果可终结对象X是对可终结对象Y唯一存在的引用,并且没有任何东西引用X,则当X.Finalize()运行时,对象Y可能已经执行其Finalize()例程,但保证对象Y仍然存在。 - supercat

1

当一个对象拥有IDisposable资源并被终结时,以下至少有一种情况适用于每个资源:

  1. 已经被终结,此时不需要清理。
  2. 它的终结器还没有运行但是已计划要运行,此时不需要清理。
  3. 只能在特定线程(不是终结器线程)中清理,此时终结器线程不能尝试清理。
  4. 仍可能被其他人使用,此时终结器线程不能尝试清理。

在极少数情况下,如果以上四种情况都不适用,则可能适合在终结器中进行清理,但除非首先检查了上述四种可能性,否则不应考虑在终结器中进行清理。


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