在编写对Stream
(s) 进行操作的函数时,有不同的递归概念。第一个简单的意义在编译器级别上不是递归的,因为如果没有立即评估尾部,那么函数会立即返回,但返回的流是递归的:
final def simpleRec[A](as: Stream[A]): Stream[B] =
if (a.isEmpty) Stream.empty
else someB(a.head) #:: simpleRec(a.tail)
上述递归概念不会引起任何问题。第二个在编译器级别上是真正的尾递归:
@tailrec
final def rec[A](as: Stream[A]): Stream[B] =
if (a.isEmpty) Stream.empty // A) degenerated
else if (someCond) rec(a.tail) // B) tail recursion
else someB(a.head) #:: rec(a.tail) // C) degenerated
这里的问题是C)
编译器将这种情况检测为非tailrec调用,即使没有执行实际调用。这可以通过将流尾分解为辅助函数来避免:
@tailrec
final def rec[A](as: Stream[A]): Stream[B] =
if (a.isEmpty) Stream.empty
else if (someCond) rec(a.tail) // B)
else someB(a.head) #:: recHelp(a.tail)
@tailrec
final def recHelp[A](as: Stream[A]): Stream[B] =
rec(as)
在编译时,这种方法最终会导致内存泄漏。由于尾递归rec
最终是从recHelp
函数中调用的,因此函数的堆栈帧recHelp
持有对蒸汽头的引用,并且在调用返回之前不会让流被垃圾收集rec
,这可能会很长(就递归步骤)取决于对B)
.
请注意,即使在无帮助的情况下,如果编译器允许 @tailrec,内存泄漏可能仍然存在,因为惰性流尾实际上会创建一个匿名对象,该对象持有对流头的引用。