递归在程序设计中是一种很有用的方法,它可以将复杂的过程用易于理解的方式转化和描述。举个例子,比如我们想要写一个从 0 累加到 n 的函数,如果我们不知道等差数列求和公式的话,就可以用递归的方式来做:

func sum(n: UInt) -> UInt {
    if n == 0 {
        return 0
    }
    return n + sum(n - 1)
}

sum(4) // 10
sum(100) // 5050

看起来没问题。但是我们如果用一个大一点的数的话,运行时就会出现错误,比如

sum(1000000)
// EXC_BAD_ACCESS (code=2, address=...)

这是因为每次对于 sum 的递归调用都需要在调用栈上保存当前状态,否则我们就无法计算最后的 n + sum(n - 1)。当 n 足够大,调用栈足够深的时候,栈空间将被耗尽而导致错误,也就是我们常说的栈溢出了。

一般对于递归,解决栈溢出的一个好方法是采用尾递归的写法。顾名思义,尾递归就是让函数里的最后一个动作是一个函数调用的形式,这个调用的返回值将直接被当前函数返回,从而避免在栈上保存状态。这样一来程序就可以更新最后的栈帧,而不是新建一个,来避免栈溢出的发生。在 Swift 2.0 中,编译器现在支持嵌套方法的递归调用了 (Swift 1.x 中如果你尝试递归调用一个嵌套函数的话会出现编译错误),因此 sum 函数的尾递归版本可以写为:

func tailSum(n: UInt) -> UInt {
    func sumInternal(n: UInt, current: UInt) -> UInt {
        if n == 0 {
            return current
        } else {
            return sumInternal(n - 1, current: current + n)
        }
    }

    return sumInternal(n, current: 0)
}

tailSum(1000000)

但是如果你在项目中直接尝试运行这段代码的话还是会报错,因为在 Debug 模式下 Swift 编译器并不会对尾递归进行优化。我们可以在 scheme 设置中将 Run 的配置从 Debug 改为 Release,这段代码就能正确运行了。