如果我们没有析构函数,为什么应该调用SuppressFinalize?

34

我有几个问题,但是找不到合适的答案。

1)当我们没有析构函数时,为什么需要在Dispose函数中调用SuppressFinalize。

2)Dispose和finalize用于在对象被垃圾回收之前释放资源。无论是托管资源还是非托管资源,我们都需要释放它,那么为什么我们需要在dispose函数中使用条件语句,说在从IDisposable:Dispose调用此重写函数时传递'true',在从finalize中调用时传递false。

请查看我从网络上复制的以下代码。

class Test : IDisposable
   {
     private bool isDisposed = false;

     ~Test()
     {
       Dispose(false);
     }

     protected void Dispose(bool disposing)
     {
       if (disposing)
       {
         // Code to dispose the managed resources of the class
       }
       // Code to dispose the un-managed resources of the class

       isDisposed = true;
     }

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

如果我删除布尔值保护的Dispose函数并实现如下,会怎样呢?
   class Test : IDisposable
   {
     private bool isDisposed = false;

     ~Test()
     {
       Dispose();
     }


     public void Dispose()
     {
      // Code to dispose the managed resources of the class
      // Code to dispose the un-managed resources of the class
      isDisposed = true;

      // Call this since we have a destructor . what if , if we don't have one 
       GC.SuppressFinalize(this);
     }
   }       
5个回答

29

我在这里冒险说一句,但是......大多数人不需要完整的Dispose模式。它的设计是为了在直接访问非托管资源时(通常通过IntPtr)以及面对继承时保持稳固。但实际上很少需要处理这两种情况。

如果你只是持有另一个实现了IDisposable接口的东西的引用,你几乎肯定不需要finalizer - 直接持有资源的对象负责处理它。你可以使用类似于下面这样的代码:

public sealed class Foo : IDisposable
{
    private bool disposed;
    private FileStream stream;

    // Other code

    public void Dispose()
    {
        if (disposed)
        {
            return;
        }
        stream.Dispose();
        disposed = true;
    }
}

请注意,这并不是线程安全的,但这可能不会成为问题。

通过不必担心子类直接持有资源,您无需抑制终结器(因为没有终结器),也无需为子类提供自定义处理的方法。没有继承会使生活更简单。

如果您确实需要允许不受控制的继承(即您不愿打赌子类将具有非常特殊的需求),则需要采用完整模式。

请注意,使用.NET 2.0中的SafeHandle时,您需要自己的终结器的情况比.NET 1.1中更少见。


针对您关于为什么首先有一个disposing标志的观点:如果您正在终结器中运行,您引用的其他对象可能已经被终结。您应该让它们自己清理,并且只清理您直接拥有的资源。


如果父类没有清理终结器,那么从非平凡父类派生的子类是否有任何合法的理由具有清理终结器?我想不出任何情况,在这种情况下,派生类将未托管资源封装到其自己的类中,并完全与主类分离。实际上,即使只想要一个“警报铃声”终结器,将其封装到自己的类中可能会更好,而不是向派生类添加终结器。 - supercat
@supercat:是的,那很可能是情况...尽管“警报铃”部分需要在每个对象中添加一个额外的字段,否则可以避免。 - Jon Skeet
...缓存争用,但仍比向对象添加终结器更便宜。实际上,通过让工厂持有对单元素数组的引用数组并随机选择一个传递给每个创建的实例,可以减少缓存争用,从而将争用分散在多个资源之间。 - supercat
4
我比“大多数人不需要完整的Dispose模式”更进一步:没有人需要它,因为直接将需要清理的托管和非托管资源作为字段持有是一种反模式。虽然“dispose模式”是处理这种情况的聪明方式,但这只是在处理你做了愚蠢的事实时的一种聪明方式。任何人使用它的唯一理由是当继承自某个强制使用它的东西时。 - Jon Hanna
@JonHanna:当终结器运行时,它将能够检查WeakReference是否已失效(如果可终结对象不持有主对象的强引用,则终结器或其他任何东西都不应阻止主对象停止存在)。如果WeakReference尚未失效,则终结器应将其调用视为虚警并重新注册自己以进行终结。这样做可以防止恶意外部代码使用复活来欺骗对象的终结器在对象正在使用时运行。 - supercat
显示剩余16条评论

9

以下是主要信息:

1) 如果你的类有Finalizer,则需要重写Object.Finalize方法, ~TypeName() 析构函数方法只是简写方式 'override Finalize()'等。

2) 在进行资源释放时(例如从using块中退出)在Dispose方法中调用GC.SuppressFinalize。如果没有Finalizer,那么不需要这样做。如果有Finalizer,则确保对象被移出Finalization队列(因为Finalizer通常也会调用Dispose方法,所以我们不需要重复处理)。

3) 实现Finalizer作为“安全机制”。Finalizer一定会运行(只要CLR未中止),因此它们允许您确保代码在Dispose方法未被调用时得到清理(例如,程序员可能忘记将实例创建在一个'using'块内等)。

4) Finalizer很昂贵,因为具有Finalizer的类型不能在Generation-0集合(最有效的集合)中进行垃圾回收,并且通过在F-Reachable队列上引用它们来提升至Generation-1,以表示GC根。直到GC执行Generation-1集合才会调用Finalizer并释放资源,因此仅在非常重要的情况下实现Finalizer,并确保需要Finalization的对象尽可能小,因为可由可终结对象到达的所有对象也将升级为Generation-1。


6
保留第一个版本,它更安全,也是dispose模式的正确实现。
1.调用SuppressFinalize告诉GC您已经完成了所有自己的销毁/处理(由类持有的资源),并且不需要调用析构函数。 2.如果使用您的类的代码已经调用了dispose,则需要进行测试,并且不应该告诉GC再次进行dispose。
请参见MSDN文档(Dispose方法应调用SuppressFinalize)。链接:this

是的,我确实理解SupressFinalze将防止GC调用Finalize。但我的疑问是,当我没有析构函数时,为什么我们需要调用SupressFinalze。因为Finalize只会为那些在finalizer队列中的对象调用,而我的对象没有析构函数,所以无论如何GC都不会调用。第二点是,为什么处理模式坚持要有一个重载的dispose函数,它接受一个布尔值,控制托管或非托管资源的释放。当对象将被处理时,为什么我们需要单独处理资源,让我们全部释放。 - somaraj
2
只有在需要使用终结器或允许子类拥有终结器时,该规则才是相关的。在许多情况下,这并不适用。 - Jon Skeet
7
重点是,您的类可能没有finalizer,但是一个子类可能有。 - Jon Skeet
1
@JonSkeet:为什么要抑制子类的 finalizer?因为在子类的 Dispose() 中已经调用了 base.Dispose(),所以应该已经有一个 GC.SupressFinalize。 - adrianm
@adrianm:如果您已经被处理并且合理地假定所有所需的清理都已完成,那么您将希望抑制终结。GC.SuppressFinalize()通常在无参数Dispose方法中,而不是具有“disposing”参数的虚拟方法-在标准(长)模式中。 - Jon Skeet
1
@adrianm:理想情况下,SupressFinalize()应该在所有派生类完成其清理逻辑之后才被调用。唯一知道这一点的是非虚拟包装器。 - supercat

3
你应该总是调用SuppressFinalize(),因为你可能有(或将来可能有)一个实现了Finalizer的派生类 - 在这种情况下,你需要它。
假设你有一个没有Finalizer的基类,并且你决定不调用SuppressFinalize()。然后3个月后你添加了一个添加了Finalizer的派生类。你很可能会忘记去基类中添加一个调用SuppressFinalize()的语句。如果没有Finalizer,调用它也没有什么坏处。
我建议使用的IDisposable模式发布在这里:如何正确实现Dispose模式

2

1. 第一个问题的答案

基本上,如果你的类没有析构方法(Destructor),就不需要调用SuppressFinalize方法。我相信人们即使在没有析构方法的情况下也会调用SupressFinalize,是因为缺乏知识。

2. 第二个问题的答案

Finalize方法的目的是释放非托管资源。最重要的一点是,当对象处于终结队列中时,将调用Finalize方法。垃圾收集器会收集所有可以被销毁的对象。在销毁之前,垃圾收集器会将已经完成终结的对象添加到终结队列中。还有另一个.NET后台进程,用于调用位于终结队列中的对象的finalize方法。当后台进程执行finalize方法时,该特定对象的其他托管引用可能已经被销毁。因为在终结执行方面没有特定的顺序。所以,Dispose模式希望确保finalize方法不尝试访问托管对象。这就是为什么托管对象在“if(disposing)”子句中,对于finalize方法来说是无法访问的。


1
有一个分析器规则要求您调用SuppressFinalize:https://learn.microsoft.com/zh-cn/visualstudio/code-quality/ca1816?view=vs-2019 - NN_

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