尾调用原则是指可以重复使用调用函数的栈框架。然而,有些运行环境(如Java虚拟机)缺乏使尾调用的栈帧重用变得有效的原语。因此,生产质量的Scala实现只需要重用最后一个动作为对自身的调用的直接递归函数的栈帧。其他尾调用也可能被优化,但不能在实现之间依赖此功能。 Tail recursion是尾调用的一种特殊情况。 直接尾递归是尾递归的一种特殊情况。只有直接尾递归得到保证会被优化。其他的尾调用也可能会被优化,但这基本上只是编译器的优化。作为一种语言特性,Scala仅保证直接尾递归消除。 尾调用是子程序中的最后一个调用。
def a = {
b
c
}
在这种情况下,对于c
的调用是尾调用,对于b
的调用则不是。
尾递归是指尾调用调用了之前已经调用过的子程序:
def a = {
b
}
def b = {
a
}
这是尾递归:a
调用 b
(一个尾调用),接着又调用了 a
。(与下面描述的直接尾递归相对应,这有时被称为间接 尾递归。)
然而,Scala 不会优化这两个示例中的任何一个。或者更准确地说:Scala 实现允许对它们进行优化,但不要求如此。这与 Scheme 不同,在 Scheme 中,语言规范保证所有以上情况将占用O(1)
栈空间。
Scala 语言规范仅保证对直接尾递归进行优化,即当子程序直接调用自身且中间没有其他调用时:
def a = {
b
a
}
在这种情况下,对
a
的调用是尾调用(因为它是子例程中的最后一个调用),它是尾递归(因为它再次调用自己),最重要的是它是直接尾递归,因为
a
直接调用自己,而不是首先通过另一个调用进行。
请注意,有许多微妙的事情可能导致方法不是直接尾递归。例如,如果
a
被重载,则递归实际上可能会通过不同的重载进行,因此将不再是直接递归。
在实践中,这意味着两件事情:
1.您无法对尾递归方法执行Extract Method Refactoring,至少不包括尾调用,因为这将把一个直接尾递归方法(它将得到优化)变成一个间接尾递归方法(它将不会得到优化)。
2.您只能使用直接尾递归。尾递归下降解析器或状态机,这可以使用间接尾递归非常优雅地表示,都不行。
这主要是因为当底层执行引擎缺乏强大的控制流操纵功能,如GOTO、continuations、first-class mutable stack或proper tail calls时,您需要在其上实现自己的堆栈、使用trampolines、进行全局CPS转换或类似麻烦的东西,以提供广义适当的尾呼叫。所有这些都对性能或与同一平台上其他代码的互操作性有严重影响。
或者,正如Clojure的创建者Rich Hickey在面临相同问题时所说:“性能、Java互操作性、尾调用。选择两个。”Clojure和Scala都选择在尾递归方面做出妥协,并且仅提供尾递归而不是完整的尾调用。
长话短说:是的,您发布的特定示例将得到优化,因为它是直接尾递归。您可以通过在方法上放置
@tailrec
注释来测试此功能。注释不会更改方法是否得到优化,但它确实保证如果无法优化该方法,则会收到编译错误。
由于上述微妙之处,通常最好在需要优化的方法上放置
@tailrec
注释,既为了获得编译错误,也为了向维护您的代码的其他开发人员提供提示。