C#我的析构函数没有被调用?

3

我有这个简单的代码并试图调用析构函数,但我无法调用它 :(

我知道GarbageCollector在必要时运行,所以我使用了GC.WaitForPendingFinalizers(); 但它也没有起作用。

这是我的代码:

class Program
    {
        static void Main(string[] args)
        {
            Calculator calculator = new Calculator();
            Console.WriteLine("{0} / {1} = {2}", 120, 15, calculator.Divide(120, 15)

            GC.Collect();
            GC.WaitForPendingFinalizers();

            Console.WriteLine("Program finishing");                           
        }

  }

class Calculator
    {

        // Constructor
        public Calculator()
        {
            Console.WriteLine("Calculator being created");
        }

        // Public Divide method
        public int Divide(int first, int second)
        {
            return first / second;
        }

        // Destructor
        ~Calculator()
        {
            Console.WriteLine("Destructor is called");

        }

    }

以下是我的输出:

正在创建计算器

120 / 15 = 8

程序结束

我做错了什么?为什么我看不到 "Destructor is called"?


1
这不是真的:https://learn.microsoft.com/zh-cn/dotnet/csharp/programming-guide/classes-and-structs/destructors - Zach Sadler
请提供相关的 https://dev59.com/MG445IYBdhLWcg3wXZIy 链接。 - Mike G
2
C#中的终结器不能保证一定会运行。在我的机器上,使用你的代码(VS 2017,Debug,Any CPU),终结器被执行并打印出“Destructor is called”。 - Andrew
您可以在此处了解GC的功能 https://www.codeproject.com/Articles/1095402/Garbage-Collection-and-Csharp - Sudarsh
3
规范正式称这些为“析构函数”。称它们为“终结器”是常见的且完全可以接受的。微妙的区别在于,终结器通常被认为是非确定性的,而析构函数则是确定性的,但这种区别完全被 C# 社区所忽略,假设 C# 程序员理解这种区别是不明智的。我会交替使用这些术语。 - Eric Lippert
显示剩余3条评论
3个回答

12

一个本地变量的寿命是声明它的本地变量范围内控制激活的寿命。因此,只要主函数结束前,本地变量就一直存在。这就足以解释为什么它不会被垃圾回收,但在这里还有一些微妙的问题需要更深入地探讨。

通过各种机制(包括通过lambda捕获外部变量、迭代器块、异步方法等)可以延长生命周期。

如果JIT能证明缩短生命周期对单线程控制流没有影响,那么可以缩短生命周期(您可以使用KeepAlive确保避免这种情况发生)。

在您的情况下,运行时可以注意到该本地变量已经没有再次进行读取,将其标记为死亡状态并放弃对对象的引用,然后进行垃圾回收和终结处理。这并非必须执行,而且显然在您的情况下没有执行。

正如另一个答案所正确指出的:如果检测到调试器正在运行,GC将有意抑制此优化,因为在调试器中检查包含对它的引用的变量时,对象被收集会给用户带来不好的体验!

让我们考虑一下关于缩短生命周期的说法所带来的影响,因为我认为您可能没有完全理解这些影响。

  • 运行时可注意到构造函数从未访问它

  • 运行时可以注意到divide从未访问它

  • 因此,该本地变量实际上从未被读取和使用过

  • 因此,在其整个生命周期中,对象不需要根据GC进行管理

  • 这意味着垃圾回收器可以在构造函数之前运行终结处理程序

GC和finalizer在它们自己的线程上运行,记住; 操作系统可以在任何时候挂起主线程并切换到GC和finalizer线程,包括在分配器运行之后但在控制权传递给构造函数之前。
在像你写的这种场景中,绝对允许发生疯狂的事情; 最后处理程序未运行只是最小的问题! 当它可能 运行时,这才是可怕的。
如果这个事实不是立刻清楚的话,那么你就没有写一个finalizer的业务。编写正确的finalizer是C#中最难做的事情之一。 如果您不是CLR垃圾收集器语义的所有细节方面的专家,则不应编写finalizer。
如需更多有关编写finalizer的困难性的想法,请参阅我有关此主题的一系列文章,该系列文章始于此处: https://ericlippert.com/2015/05/18/when-everything-you-know-is-wrong-part-one/

5
老实说,使用现代工具集,您永远不需要编写终结器。任何需要终结器的东西都可以用自定义或预先制作的SafeHandle来包装起来。请参阅由Stephen Cleary撰写的文章“IDisposable:关于资源释放,你母亲从未告诉过你”的指南。 - Scott Chamberlain
1
@EricLippert 感谢您的回答。当然我并没有写这样的代码。实际上,我是在尝试理解GC和finalizer的工作原理。通过您博客中的文章,我更好地理解了这两个概念。在我成为Facebook的软件工程师之前,我不会尝试使用finalizer :P :) - Uur
4
我正在考虑是否应该在 Facebook 的 PHP/Hack 代码中删除析构函数,所以如果你来到这里,可能就没有机会编写析构函数了。 :-) - Eric Lippert
我目前正在考虑是否要在FB的PHP/Hack代码中删除析构函数的用法。那么...它们被删除了吗? :) @EricLippert - Gabriel
1
@Gabriel:老实说,我不记得了。 - Eric Lippert

1
如果您在调试器附加的情况下运行程序,它会改变对象生命周期的行为。
没有调试器时,对象在代码中的最后一次使用已经过去后就可以被收集。但是,在调试器附加的情况下,所有对象的生命周期都会延长到对象所在作用域的整个时间。这样做是为了您可以在调试器的“监视”窗口中查看对象,而不必担心对象被收集掉。
您必须在不附加调试器的发布模式下运行程序,或在调用GC.Collect()之前将calculator设置为null,以便使对象符合垃圾回收条件并运行其终结器。

1
虽然这个答案犯了混淆对象和包含引用它们的变量的常见错误,但基本上是正确的:垃圾回收器知道调试状态并故意减少操作。为什么要点踩? - Eric Lippert
1
@EricLippert 我觉得这是对所有答案的大规模点踩,我的回答和其他两个(其中一个已被删除)同时收到了一个点踩。 - Scott Chamberlain
1
@ScottChamberlain,另外两个回答都是错误的。更好的问题是为什么有人现在赞同那些明显不正确的答案。 - Servy
@Servy 我同意,但我认为我被归为他们的一员,而没有人读到我实际写的内容。 - Scott Chamberlain
1
@ScottChamberlain,所以你认为有人仔细阅读了两个答案,意识到它们是错误的,然后将它们投下反对票,而没有阅读另一个答案?这似乎是一个奇怪的假设。我认为更有可能的是一些人实际上相信那些不正确的答案是正确的,不知道正确的答案,并因此投下了反对票。 - Servy

0

我不建议依赖.NET的析构函数。

无论如何,在您的情况下,垃圾收集器在调用GS时不认为您的对象是垃圾,因为您在堆栈中有一个存在的链接,指向堆中的对象,所以您可以尝试修改此代码。

main(){
  DoCalculations();
  //at this point object calculator is garbage (because it was allocated in stack)
  GC.Collect();
}
DoCalculations(){
  Calculator calculator = new Calculator(); // object allocated
  calcualtor.doSomething();  //link alive
}

垃圾回收器可以在证明一个本地变量(或任何变量)的值不会再被读取时,立即将其视为死亡,即使它仍然在作用域内。(当然,这并非必须这样做,但是允许这样做。) - Servy
@Roland Pashinev,非常感谢,它运行得很好。即使在GC.Collect运行时它不是垃圾,但在我的代码结束时它不是垃圾吗? - Uur

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