【问题标题】:c# my destructor isn't being called?c#我的析构函数没有被调用?
【发布时间】:2017-11-18 07:05:07
【问题描述】:

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

我知道 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

节目整理

我做错了什么?为什么我看不到“调用了析构函数”?

【问题讨论】:

标签: c# destructor finalize


【解决方案1】:

局部变量生命周期是在声明它的局部变量范围内激活控件的生命周期。所以你的本地人一直活着,直到 main 结束。仅此一点就足以解释为什么不收集它,但这里有一些微妙之处需要我们更深入地探索。

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

允许在抖动可以证明这样做对单线程控制流没有影响的情况下缩短生命周期。 (您可以使用KeepAlive 确保在必须避免的情况下不会发生这种缩短。)

在您的情况下,允许运行时注意到本地不再被读取,将其标记为提前死亡,从而孤立对象的引用,然后将被收集并敲定。这样做不是必需,显然,在你的情况下,不需要。

另一个答案正确地指出:如果 GC 检测到调试器正在运行,它会故意抑制这种优化,因为当您检查包含对它的引用的变量时,收集对象是一种糟糕的用户体验调试器!

让我们考虑一下我关于缩短寿命的陈述的含义,因为我认为您可能还没有完全理解这些含义。

  • 允许运行时注意到 ctor 永远不会访问它

  • 允许运行时注意到 divide 永远不会访问此

  • 允许运行时注意到,因此本地永远不会被实际读取和使用

  • 因此允许对象在其生命周期中的任何时候都不能植根于 GC

  • 这意味着允许垃圾收集器在构造函数之前运行终结器

GC 和终结器在各自的线程上运行,记住;操作系统可以在任何时候挂起主线程并切换到 gc 和 finalizer 线程,包括在分配器运行之后但控制权传递给构造函数之前。

在你所写的场景中允许发生绝对疯狂的事情;终结器未运行是您的最小问题!正是当它可以运行时才可怕。

如果您没有立即清楚这一事实,那么您就没有必要编写终结器了。编写正确的终结器是 C# 中最难的事情之一。如果您不是 CLR 垃圾收集器语义的所有细节方面的专家,那么您不应该编写终结器。

有关编写终结器有多困难的更多想法,请参阅我关于该主题的系列文章,从这里开始:

https://ericlippert.com/2015/05/18/when-everything-you-know-is-wrong-part-one/

【讨论】:

  • 老实说,使用现代工具集,您将永远不需要编写终结器,永远。任何需要整理器的东西都可以用定制的或预制的SafeHande 包裹起来。请参阅文章“IDisposable: What Your Mother Never Told You About Resource Deallocation”,了解 Stephen Cleary 撰写的精彩指南。
  • @EricLippert 感谢您的回答。我当然不是在写这样的代码。事实上,我试图了解 GC 和终结器是如何工作的。通过您博客中的文章,我可以更好地理解这两个。在我成为 facebook 的软件工程师之前,我不会尝试使用终结器 :P :)
  • @Uur:我目前正在讨论是否在 FB 的 PHP/Hack 代码中删除析构函数的使用,所以如果你真的来到这里,你可能没有机会编写析构函数。 :-)
【解决方案2】:

如果你运行一个附加了调试器的程序,它会改变对象生命周期的行为。

在没有调试器的情况下,只要在代码中传递了对象的最后一次使用,对象就可以被收集起来。附加调试器后,所有对象的生命周期都会延长到对象在范围内的整个时间,这样做是为了您可以在调试器的Watch 窗口中查看对象,而不会从您的下方收集对象。

您必须在没有附加调试器的情况下在发布模式下运行程序,或者在调用GC.Collect() 之前将calculator 设置为null 才能使对象有资格进行垃圾收集并运行它的终结器。

【讨论】:

  • 虽然这个答案使得将对象和包含对它们的引用的变量混为一谈,但它基本上是正确的:GC 知道它何时被调试并且故意不那么激进。为什么投反对票?
  • @EricLippert 我认为这是对所有答案的大规模否决,我的和另外两个(现已删除)都同时投了反对票。
  • @ScottChamberlain 好吧,其他两个人都错了。更好的问题是为什么有人现在对那些客观不正确的答案之一投了赞成票。
  • @Servy 我同意,但我认为我在没有人阅读我实际写的内容的情况下与他们混为一谈。
  • @ScottChamberlain 所以你认为有人仔细阅读了两个答案,意识到他们错了,对他们投了反对票,然后在没有阅读的情况下对另一个答案投了反对票?这似乎是一个奇怪的假设。我想说的是,有些人实际上认为那些错误的答案是正确的,不知道正确的答案,并因此而对你的答案投了反对票,这更合理。
【解决方案3】:

我不建议真正使用析构函数 .net

无论如何,在您调用 GS 时,GC 不会认为您的对象是垃圾,因为您的堆栈中有 活动链接 计算器,它指向堆中的对象 所以你可以尝试修改这段代码

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
}

【讨论】:

  • 允许 GC 认为一个局部变量(或与此相关的任何变量)已经死亡,只要它可以证明它的值将永远不会被再次读取,即使它仍在范围内。 (当然不是必须这样做,但允许这样做。)
  • @Roland Pashinev 它工作得很好,谢谢。即使 GC.Collect 运行时它不是垃圾,我的代码末尾不是垃圾吗?
猜你喜欢
  • 2011-11-23
  • 2015-12-23
  • 1970-01-01
  • 1970-01-01
  • 2016-12-31
  • 2013-07-10
  • 1970-01-01
  • 2012-09-17
  • 2019-10-05
相关资源
最近更新 更多