为什么Rust中不建议递归?

  • 本文关键字:递归 Rust recursion rust
  • 更新时间 :
  • 英文 :


我熟悉关于递归的一般意识-不要使用它,因为它不是一个好的内存管理实践。然而,这个概念应该适用于所有的编程语言,除非它们在递归下处理内存管理非常好。

在浏览Educative的Rust课程的文档时,有一个声明:

递归在Rust中是可能的,但实际上不鼓励这样做。相反,Rust倾向于一种叫做迭代的东西,也被称为循环。

我无法理解为什么会这样?与其他语言相比,Rust中是否有不太常见的东西,比如不建议递归,或者在Rust中处理比在其他语言中要好?

我无法理解为什么会这样?与其他语言相比,Rust中是否有一些不太常见的东西,比如不建议递归,或者Rust中的迭代处理得比其他语言好?

大多数语言都非常喜欢"从迭代到递归,他们只是懒得这么显式地解释。例如,Python对递归深度有解释器级的限制,默认值为1000。

对于Rust来说,这一点可能会被明确指出,因为它的起源部分在于函数式语言,而函数式语言基本上是唯一喜欢递归的语言:许多结构受到ml和Haskell的启发,最初的编译器是OCaml,我认为甚至都没有迭代结构(也就是任何类型的forwhile循环)。


至于为什么一种语言喜欢迭代而不喜欢递归:没有尾部调用消除(TCE),递归将导致堆栈空间的消耗,可能是无界的。

如果语言使用C堆栈,除非它还设置自己的递归限制,它没有办法轻易地知道堆栈可以多深:默认值可高达8 mb在Linux (glibc真的)和低至64 k OpenBSD(至少在OpenBSD 4)的日子,和很少的方法用于查询这些信息,没有真正的清洁检查方法(加上检查不会是免费的)。更有问题的是,堆栈溢出在最好的情况下会导致段错误(硬崩溃程序),在更坏的情况下会导致内存不安全,所以这是您真正想要避免的事情。

即使没有这个——再次假设你没有TCE——递归实现的无界循环(例如事件循环)将消耗无界的内存,因为每个递归调用将保留一个堆栈帧,该堆栈帧将永远不会被回收,因为函数永远不会返回。

无限迭代循环可能会消耗100%的CPU(如果你不做任何等待IO事件或锁的事情),但不会消耗所有的内存,除非你特别明确地积累数据(例如将东西推入向量)。


如果你TCE,那么从技术上来说你根本不需要迭代,事实上有很多语言都不需要迭代。据我所知,OCaml和Erlang(这不是一个详尽的列表,但这两个我相当确定)没有任何类型的循环结构,只要你的递归调用在尾部位置,它就会被优化掉。

后一点使它稍微复杂,并且容易出错:对于TCE工作,调用必须位于尾部位置,这意味着您要做的最后一件事:foo()是尾部调用,但1 + foo()不是,加法位于尾部位置而不是调用。这意味着很容易错误地使函数非尾递归,并且您经常需要将循环具体化为辅助的基于累加器的函数来解决这个问题(例如,在上面的情况下,您将调用foo2(1)而不是1 + foo(),它将在内部执行增量,类似这样)。语言设计者和实现者需要指定和实现TCE,这需要一些努力,可能会限制您的选择,等等…


最后,在回答中,我专门讨论了尾部呼叫消除(TCE)。你可能听说过尾部调用优化(TCO), Rust有它,因为它的后端是LLVM,有TCO。

TCO的问题优化意味着启发式和偶然因素:编译器可能会或可能不会优化递归,这取决于调用类型,函数大小,参数的数量(和大小),控制流等……,并且优化甚至可能不会在所有后端实现。这意味着TCO不能作为语言语义的基础如果您要使您的语言在没有迭代构造的情况下工作,您需要在尾部调用出现时可靠地删除。因此,你需要尾部呼叫消除,而不仅仅是优化。

最新更新