【问题标题】:Why .NET Object has method Finalize()?为什么 .NET 对象有方法 Finalize()?
【发布时间】:2017-10-11 14:50:03
【问题描述】:

我知道垃圾收集器使用 Finalize 方法让对象释放非托管资源。据我所知,Object.Finalize 永远不会被 GC 直接调用(如果它的类型 overrides 通过实现终结器来实现 Finalize 方法,则在它的构造过程中将对象添加到 f-reachable 队列中)。

Object.Finalize 仅从自动生成的终结器代码调用

try
{
  //My class finalize implementation
}
finally
{
  base.Finalize(); // Here chain of base calls will eventually reach Object.Finalize/
}

因此,拥有一个派生自 Object 的任意类不会调用 Object.Finalize - 您需要 Object.Finalize 的终结器才有意义,而对于大多数类来说,它没有意义并且未被使用(并不是说它的实现是实际上是空的)。

如果不覆盖 Object.Finalize 以及在不尝试{}finally{base.Finalize()} 通话?类似于 Add 方法的集合初始化方法 - 您不必实现任何接口或重写该方法 - 只需实现 public void Add(item) 方法即可。

这会使 C# 编译器稍微复杂一点,但通过删除一个冗余调用使终结器运行得稍微快一些,最重要的是 - 使 Object 类更易于理解,而无需使用具有空实现的受保护 Finalize 方法,而它不需要完成任何事情。

还可以实现从 Object 派生的 FinalizableObject 类,并使编译器派生所有具有终结器的类。它可以实现 IDisposable 并使disposing pattern, recommended by Microsoft 可重用,而无需在每个类中实现它。其实我很惊讶这样的基类不存在。

【问题讨论】:

  • 是什么让你认为你“需要在每个类中实现 [IDisposable]”?这绝对是不是正确的。
  • @JoelCoehoorn 我并没有说我们在每个班级都需要 IDisposable。我们当然不会。所以我的问题是为什么我们在每个类中都有 Finalize(继承自 Object)?

标签: c# .net garbage-collection finalizer


【解决方案1】:

编辑

垃圾回收不会调用Object.Finalise 的子实现,除非该方法被覆盖。为什么它对所有对象都可用?这样它可以在需要时被覆盖,但除非它不会影响性能。查看文档here,它指出;

Object 类不提供 Finalize 方法的实现,并且垃圾收集器不会标记派生自 Object 的类型以进行终结,除非它们覆盖 Finalize 方法。

关于完成的说明

直接引用 Ben Watson 的优秀著作《Writing High-Performance .NET Code》,因为他的解释比我做得更好;

除非需要,否则永远不要实现终结器。终结器是代码,由垃圾收集器触发以清理非托管资源。它们是从单个线程一个接一个地调用的,并且只有在垃圾收集器在收集后声明对象死亡之后。这意味着如果你的类实现了一个终结器,你保证即使在应该杀死它的集合之后它也会留在内存中。这会降低整体 GC 效率并确保您的程序将 CPU 资源专用于清理对象。

如果你实现了终结器,你还必须实现IDisposable 启用显式清理的接口,并调用GC.SuppressFinalize(this)Dispose 方法中将对象从终结队列中移除。 只要在下一次收集之前调用Dispose,它就会正确清理对象,而无需运行终结器。下面的例子正确地演示了这种模式;

class Foo : IDisposable
{
    ~Foo()
    {
        Dispose(false);
    }

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

    protected virtual void Dispose(bool disposing)
    {
        if (disposing)
        {
            this.managedResource.Dispose();
        }
        // Cleanup unmanaged resource
        UnsafeClose(this.handle);
        // If the base class is IDisposable object, make sure you call:
        // base.Dispose();
    }
}

注意 有些人认为终结器可以保证运行。这通常是正确的,但并非绝对如此。如果程序被强制终止 然后不再运行代码,进程立即终止。在进程关闭时,所有终结器的时间也有时间限制。如果你的终结器在列表的末尾,它可能会被跳过。而且, 因为终结器是按顺序执行的,如果另一个终结器中有一个无限循环错误,那么它之后的终结器将永远不会运行。虽然终结器不在 GC 线程上运行,但它们由 GC 触发,因此如果您没有集合,终结器将不会运行。因此,您不应依赖终结器来清理进程外部的状态。

Microsoft 对终结器和 Disposable 模式 here 有一篇很好的文章

【讨论】:

  • Never implement a finalizer unless it is required. 没错。为了进一步实现这一点,唯一需要的是当您构建一种全新的非托管资源时。示例:如果您正在构建,您确实不需要需要终结器将非托管数据库连接资源包装在 SqlConnection 中的数据访问层(尽管它可能确实需要 IDisposable),因为 SqlConnection 类型已经为您完成了任务。如果您正在为需要清理的全新数据库引擎构建连接类,您只需要一个终结器。
  • 我个人觉得 Stephen Cleary 的 this article 比微软自己的文档更好。它谈到将一次性课程分为两种类型。具有非托管资源并从 SafeHandle 派生的资源以及仅拥有 IDisposeable 对象(SafeHandle 是 IDisposeable)的资源。使用这种模式,您将永远不需要编写终结器。
  • @JoelCoehoorn “唯一需要的时候是当你构建一种全新的非托管资源时” 我不同意,我有足够的信心说你应该 从不编写终结器。任何需要终结器的东西都应该用SafeHande 封装,而不是从SafeHandle 派生的类应该只包含托管资源。
  • 您的讨论是指导我们何时需要实现终结器,但最初的问题是一个有点不同的领域 - 为什么微软将 Finalize 方法添加到 Object 类而不是在 CLR 引擎和 C# 编译器中进行更多的编码和使 Object 类清除它不需要、不使用和不实现的终结逻辑?我错过了什么吗?
  • @ScottChamberlain 因为我是疯狂的表演者,而且 OP 确实谈到了加快完成速度,我觉得有义务指出您可以使用 CriticalHandle 而不是 SafeHandle 达到相同的效果。它更快(自然有安全权衡)。我使用CriticalHandle 而不是SafeHandle,因为在我的世界中,性能往往是第一要务。
【解决方案2】:

C# 语言的析构函数语法模糊了终结器真正 的作用。也许最好用一个示例程序来演示:

using System;

class Program {
    static void Main(string[] args) {
        var obj = new Example();
        obj = null;    // Avoid debugger extending its lifetime
        GC.Collect();
        GC.WaitForPendingFinalizers();
        Console.ReadLine();
    }
}

class Base { ~Base() { Console.WriteLine("Base finalizer called"); } }
class Derived : Base { ~Derived() { Console.WriteLine("Derived finalizer called"); } }
class Example : Derived { }

输出:

调用的派生终结器
基终结器调用

这种行为有一些值得注意的地方。 Example 类本身没有终结器,但无论如何都会调用它的基类终结器。在基类终结器之前调用派生类终结器并非偶然。请注意,派生类的终结器没有调用 base.Finalize(),尽管 Object.Finalize() 的 MSDN 文章要求它这样做,但无论如何都会调用它。

您可能很容易识别这种行为,这是virtual 方法的行为方式。一个覆盖调用基本方法的方法,就像通常的虚拟方法覆盖一样。否则正是它在 CLR 中的内容,Finalize() 是一个普通的虚拟方法,就像任何其他方法一样。 C# 编译器为 Derived 类的析构函数生成的实际代码类似于:

protected override Derived.Finalize() {
    try {
        Console.WriteLine("Derived finalizer called"); 
    }
    finally {
        base.Finalize();
    }
}

无效代码,但可以从 MSIL 逆向工程的方式。 C# 语法糖确保您永远不会忘记调用基本终结器,并且它不会被线程中止或 AppDomain 卸载中止。 C# 编译器不会以其他方式帮助并为 Example 类自动生成终结器; CLR 完成必要的工作来查找最派生类的终结器,遍历基类的方法表,直到找到一个。同样,通过设置一个标志来帮助类加载器,以指示 Example 具有带有终结器的基类,因此需要由 GC 特殊处理。基类终结器调用 Object.Finalize(),即使它没有做任何事情。

所以关键是 Finalize() 实际上是一个虚方法。因此,它需要在 Object 的方法表中的一个槽,以便派生类可以覆盖它。是否可以以不同的方式完成是非常主观的。当然不容易,也不是不强制每种语言实现都对其进行特殊处理。

【讨论】:

  • 具有 3 个类层次结构的示例非常具有解释性,它证明 CLR 仍然需要遍历方法表来确定某处是否定义了终结器。同样可以通过签名搜索 finalize 方法,而不是在 Object 类中......你看到这样的想法有什么问题吗?我确定我一定监督了什么......
  • Hmya,这不是 CLR 规范所说的应该做的。它只为一个终结器调用提供保证,而不是其他任何东西。否则 C# 编译器发出这些基本调用的原因。任何地方都不需要将它们放在终结器块中,就像 C# 团队喜欢它的方式一样。所有非常明智的选择。
  • 我不反对 finally 块。我只是好奇如果没有 Object 类中的 Finalize 方法,这一切是否都可以完成。而且我找不到任何不可能的原因,现在没有人真正说出任何原因。如果只有需要终结器的对象(声明终结器和派生对象)具有虚方法 Finalize,您不同意 FCL API 会更干净吗?
  • 它不会更干净,远非如此。虚拟方法和覆盖在 MSIL 中没有相同的签名。就像它们不在 C# 语法中一样。如果 Base 没有终结器并且稍后重构为具有终结器,则任何具有从 Base 派生的终结器的类都将中断。如果那个类住在另一个程序集中,那就太糟糕了。通过给 Object 一个虚拟的 Finalize 方法,这不会出错,因为任何终结器都是 override
  • 好点。可能有足够的理由不这样做。我也有一个想法,如果是我描述的方式,不仅 CLR 应该遍历方法表,而且 C# 编译器也应该知道基类是否具有终结器,并且不确定它是否容易实现。
猜你喜欢
  • 2015-05-02
  • 1970-01-01
  • 2019-04-25
  • 2015-01-09
  • 1970-01-01
  • 1970-01-01
  • 2015-10-24
  • 1970-01-01
  • 2012-07-16
相关资源
最近更新 更多