【问题标题】:Why does this scala tail recursive function throws java.lang.StackOverflowError为什么这个scala尾递归函数会抛出java.lang.StackOverflowError
【发布时间】:2014-08-28 01:52:50
【问题描述】:

下面的简单函数是尾递归的。但是,它仍然会抛出 java.lang.StackOverflowError。谁能帮我理解我为什么会得到这个?

我已经尝试使用包含数百万项列表的尾递归函数,但从未得到StackOverflowError。还是这个函数不是尾递归的?

def recursion(i: Int): Int = {
  if (i == 20000) i
  else {
    recursion(i + 1)
  }
}

recursion(0)

更新:添加@tailrec 确实可以解决问题,即使我将限制增加到 2147483647。感谢@tiran 的回答

【问题讨论】:

  • 您可以通过在函数中添加@tailrec 注解来检查尾递归。这是尾递归,不会为我抛出 SOE。
  • 这是尾递归,但您是否可能在您的 REPL 会话/其他地方有另一个名为 recursion 的函数覆盖此函数?
  • 感谢您的建议。我创建了一个只有一个函数的测试应用程序,它称为递归。在函数中添加@tailrec 确实解决了这个问题。谢谢蒂兰
  • 实际上添加@tailrec 并不能解决问题,因为它只是一个告诉编译器I think this function is tailrecursive, but check it and tell me if it's differently 的注解。
  • @SamVo 改写 goral - 它不告诉编译器执行 TCO(尾调用优化),这将在有和没有注释的情况下完成,而是告诉编译失败以防万一不可能执行。

标签: scala tail-recursion stack-overflow


【解决方案1】:

该函数是尾递归的,实际上编译器会按预期对其进行优化。您的示例在我的 scala 2.11.2 REPL 中运行良好(我添加了几个零,只是为了更好地证明这一点)

scala> def recursion(i: Int): Int = {
     |   if (i == 2000000) i
     |   else {
     |     recursion(i + 1)
     | }
     | }
recursion: (i: Int)Int

scala> recursion(0)
res0: Int = 2000000

关于@tailrec,它不能“解决”任何事情。 @tailrec 是一个注解,在无法对注解函数执行尾调用优化 (TCO) 时触发编译时错误。

因此,无论@tailrec 注释如何,总是尽可能执行 TCO。

由于您的问题在重新编译后就消失了,因此您的 IDE 可能处于某种肮脏状态,并且编辑只是触发了干净的编译。代码从一开始就很好。

【讨论】:

    【解决方案2】:

    在每种语言中,由于存在局部差异,每次调用函数时,都会在称为“堆栈”的内存区域上创建一个新分配,以存储有关函数的信息,例如参数和返回地址。当函数返回时,您释放相应的堆栈块并返回给调用者。

    假设你有这样一个函数:

    def f(i:Integer) = {
      if (i <= 0) 0
     else f(i-1)
    }
    

    如果你调用 f(3),你会得到:

    f(3) -> 调用 -> f(2) -> 调用 -> f(1) -> 调用 -> f(0)

    当你的堆栈达到最大高度时,看起来像:

    stack(f(0))
    stack(f(1))
    stack(f(2))
    stack(f(3))
    

    一旦 f(0) 调用返回,它的块就会被释放,并且依次释放 f(1)、f(2)、f(3) 中的块,并且您的堆栈会缩小。

    程序的堆栈在任何时候都可以具有的最大高度有限制。你的函数递归了 20000 次,超过了这个限制,所以 JVM 生气并抛出异常。

    添加@tailrec 告诉计算机将您的函数编译为尾递归,即(或多或少)在不分配堆栈空间的while循环中对其进行转换,例如:

    def recursion(i:Int) = {
      while(i<20000) {
        i++
      }
      i
    }
    

    因此,您不会打破堆栈上限 :)

    编辑:正如其他人指出的那样,@tailrec 不会告诉编译器执行 TCO,而是在无法对方法执行 TCO 时要求编译器失败,从而在编译的情况下保证方法的 TCO成功。

    【讨论】:

    • 其实不正确。 @tailrec 仅告诉编译器如果无法对带注释的方法执行尾调用优化,则编译失败,但不会触发它。
    猜你喜欢
    • 1970-01-01
    • 2017-01-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多