【问题标题】:Why doesn't this obvious infinite recursion give a compiler warning? [closed]为什么这个明显的无限递归没有给出编译器警告? [关闭]
【发布时间】:2012-01-06 17:57:21
【问题描述】:

几个月前,我不得不修复一些导致一些问题的代码。代码看起来基本上是这样的:

int badFun() { return badFun(); }

这显然会导致堆栈溢出,即使在我使用的高级语言(SilkTest 中的 4Test)中也是如此。这段代码不可能被视为有益的。问题的第一个迹象是脚本完成后看到的警告,但没有编译错误或警告。奇怪的是,我尝试用相同结构的 C++、C# 和 Python 编写程序,并且所有这些程序都在没有语法错误或警告的情况下编译/解释,即使在所有情况下都存在运行时错误。在这些情况下,我什至没有看到任何警告。为什么默认情况下这不被视为可能的问题?

编辑:我尝试用所有三种语言编写该函数的等效项,因此我添加了这些函数标记。我对为什么这样的代码没有警告就能通过的总体原因更感兴趣。如有需要请重新标记。

【问题讨论】:

  • 您的问题本身是关于 C#、C++ 还是 Python?代码示例在我看来很像 C#。
  • 阻止你做一些愚蠢的事情不是编译器的工作。在大多数语言中,某些外部事件可能会导致这样的构造退出。
  • 因为防止你故意用自己的脚射击自己不是计算机的责任:fullduplex.org/humor/2006/10/… 请参阅 Lisp。
  • 一些 IDE 工具会发出警告,例如 Resharper for C# 和 Visual Studio 中的 VB.NET。
  • 这样的尾递归可能会被编译器实现为循环,因此这段代码可能不是运行时错误,而是类似于while(true);

标签: c# c++ python compiler-construction recursion


【解决方案1】:

这是交易:编译器警告是特性。功能需要努力,而努力是有限的。 (它可能以美元来衡量,也可能以某人愿意为开源项目付出的小时数来衡量,但我向你保证,它是有限的。)

因此,我们必须对这项工作进行预算。我们花在设计、实现、测试和调试一个功能上的每一个小时,都是我们本可以花在做其他事情上的一个小时。因此,我们在决定要添加哪些功能时非常谨慎。

所有功能都是如此。警告有特殊的附加问题。警告必须与具有以下特征的代码有关:

  • 合法。显然代码必须是合法的;如果它不合法,那么它首先不是警告,而是错误。
  • 几乎肯定是错的。警告您正确的、可取的代码的警告是错误的警告。 (此外,如果代码 正确,则应该有一种方法可以编写代码以使警告消失。)
  • 不明显。警告应该告诉您一些微妙的错误,而不是明显的错误。
  • 易于分析。有些警告根本不可能;例如,要求编译器解决停机问题的警告不会发生,因为这是不可能的。
  • 不太可能被其他形式的测试发现。

在您的具体示例中,我们看到其中一些条件得到满足。该代码是合法的,几乎可以肯定是错误的。但这不明显吗?有人可以很容易地看代码,发现它是一个无限递归;警告没有多大帮助。是否适合分析?您给出的简单示例是,但寻找无限递归的一般问题等同于解决停止问题。是不是不太可能被其他形式的检测发现?不会。当您在测试用例中运行该代码时,您会得到一个异常,准确地告诉您出了什么问题。

因此,我们不值得为此发出警告。我们可以通过更好的方式来支出该预算。

【讨论】:

  • +1 出色的答案,真正了解我的问题。我知道编译器不应该阻止你编写糟糕的代码,但是好的编译器至少应该抛出一个“嘿,你在做什么?”有时。
  • int BadProperty{get{return BadProperty;}} 及其等效的 setter 存在相关问题,错误很容易犯,而且不容易发现。但是这个问题在你实际运行代码时显然很容易发现,这大大降低了这个特性的好处。
  • @CodeInChaos:确实,这是警告可能合理的地方。
  • 我不同意不明显的说法——你也应该得到明显错误的警告。
【解决方案2】:

为什么默认情况下这不被视为问题?

该错误是运行时错误,而不是编译时错误。该代码是完全有效的,它只是做了一些愚蠢的事情。您展示的非常简单的案例当然可以检测到,但许多稍微复杂一点的案例很难检测到:

void evil() {
    if (somethingThatTurnsOutToAlwaysBeTrue)
        evil();
}

为了确定这是否有问题,编译器必须尝试确定条件是否始终为真。在一般情况下,我认为这比确定程序最终是否会停止(即它是provably not computable)更具可计算性。

【讨论】:

  • +1。编译器不会打扰,因为在一般情况下停止问题是无法计算的。
  • 按照同样的逻辑,编译器不会费心警告无法访问的代码。但它确实如此 - 在特定的情况下。
  • @harold 正如 Eric Lippert 很好解释的那样,它归结为关于产生警告的成本和收益的决定。我在这里想说的是,检测微不足道的情况似乎不是很有用,而警告有用的非微不足道的情况可能很难或不可能检测到。
【解决方案3】:

任何编程语言的编译器都不知道它编译的代码的语义。这是有效的代码,虽然很愚蠢,所以它会被编译。

【讨论】:

  • 编译器非常了解代码的语义。在合理的水平上实施此警告将非常容易。但我不知道是否值得指定、实施、记录和维护该功能。
【解决方案4】:

编译器或解释器如何知道函数在做什么?编译器和解释器的范围是编译或解释语法代码——而不是解释代码的语义。

即使编译器确实检查了这一点,你在哪里画线?如果你有一个永远计算阶乘的递归函数呢?

【讨论】:

  • 编译器非常了解你的函数在做什么。它知道函数中的所有代码路径。它使用这种语义知识进行分析,例如检测无法访问的代码,确保在读取所有代码路径之前初始化变量,...
【解决方案5】:

因为编译器不会检查这些东西。

如果您在Visual Studio 中安装Resharper 之类的代码分析器,它会在您启用代码分析选项的情况下发出无限递归调用或类似的警告。

【讨论】:

    【解决方案6】:

    我怀疑编译器能否在编译时检测到运行时现象(堆栈溢出)。有许多有效的情况可以在其内部调用函数,递归。但是编译器如何从递归的坏情况中知道好处呢?

    除非它添加了一些 AI,否则我认为编译器无法检测好递归和坏递归之间的差异,这是程序员的工作。

    【讨论】:

      【解决方案7】:

      正如您所提到的,编译器只检查语法错误。 递归函数完全有效,没有任何错误。

      在运行时,

      当堆栈溢出时,由于堆栈溢出 *不是因为代码*而引发错误。

      递归函数是完全有效的,但是在实现中我们需要在堆栈被填充之前进行条件检查以返回值。

      【讨论】:

        猜你喜欢
        • 2014-06-19
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2018-02-16
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多