第一版是为 Lua 5.0 编写的。虽然在很大程度上仍然适用于更高版本,但还是存在一些差异。
第四版针对 Lua 5.3,可在 亚马逊 和其他书店购买。
购买本书,您还将帮助支持 Lua 项目


6.3 – 正确的尾调用

Lua 中函数的另一个有趣特性是它们执行正确尾调用。(尽管该概念并不直接涉及递归,但一些作者使用了术语正确尾递归。)

尾调用是一种伪装成调用的 goto。当函数将其最后操作作为另一个函数的调用时,就会发生尾调用,因此它没有其他操作可做。例如,在以下代码中,对 g 的调用是尾调用

    function f (x)
      return g(x)
    end
f 调用 g 之后,它没有其他操作可做。在这种情况下,当被调用函数结束时,程序不需要返回到调用函数。因此,在尾调用之后,程序不需要在堆栈中保留任何有关调用函数的信息。一些语言实现(例如 Lua 解释器)利用了这一事实,并且在执行尾调用时实际上不会使用任何额外的堆栈空间。我们说这些实现支持正确尾调用

由于正确尾调用不使用堆栈空间,因此程序可以进行的“嵌套”尾调用的数量没有限制。例如,我们可以使用任何数字作为参数调用以下函数;它永远不会溢出堆栈

    function foo (n)
      if n > 0 then return foo(n - 1) end
    end

在使用正确尾调用时,一个微妙的要点是什么是尾调用。一些明显的候选不符合调用函数在调用后无事可做的标准。例如,在以下代码中,对 g 的调用不是尾调用

    function f (x)
      g(x)
      return
    end
该示例中的问题在于,在调用 g 之后,f 在返回之前仍必须丢弃 g 的临时结果。类似地,所有以下调用都不符合标准
    return g(x) + 1     -- must do the addition
    return x or g(x)    -- must adjust to 1 result
    return (g(x))       -- must adjust to 1 result
在 Lua 中,只有 return g(...) 格式的调用才是尾调用。但是,g 及其参数都可以是复杂表达式,因为 Lua 在调用之前对其进行求值。例如,下一个调用是尾调用
      return x[i].foo(x[j] + a*b, i + j)

正如我之前所说,尾调用是一种 goto。因此,Lua 中正确尾调用的一个非常有用的应用是编程状态机。此类应用可以用一个函数表示每个状态;更改状态就是转到(或调用)一个特定函数。例如,让我们考虑一个简单的迷宫游戏。迷宫有几个房间,每个房间最多有四个门:北、南、东、西。在每一步中,用户输入一个移动方向。如果在该方向上有一扇门,用户将转到相应的房间;否则,程序将打印一个警告。目标是从初始房间移动到最终房间。

这个游戏是一个典型的状态机,其中当前房间是状态。我们可以为每个房间实现一个函数来实现此类迷宫。我们使用尾调用从一个房间移动到另一个房间。一个有四个房间的小迷宫可能如下所示

    function room1 ()
      local move = io.read()
      if move == "south" then return room3()
      elseif move == "east" then return room2()
      else print("invalid move")
           return room1()   -- stay in the same room
      end
    end
    
    function room2 ()
      local move = io.read()
      if move == "south" then return room4()
      elseif move == "west" then return room1()
      else print("invalid move")
           return room2()
      end
    end
    
    function room3 ()
      local move = io.read()
      if move == "north" then return room1()
      elseif move == "east" then return room4()
      else print("invalid move")
           return room3()
      end
    end
    
    function room4 ()
      print("congratulations!")
    end
我们通过调用初始房间来开始游戏
    room1()
如果没有正确的尾调用,每个用户移动都会创建一个新的堆栈级别。在一些移动之后,将发生堆栈溢出。使用正确的尾调用,用户可以进行的移动次数没有限制,因为每个移动实际上执行的是到另一个函数的 goto,而不是常规调用。

对于这个简单的游戏,您可能会发现数据驱动的程序(您使用表描述房间和移动)是一个更好的设计。然而,如果游戏中每个房间都有几个特殊情况,那么这种状态机设计非常合适。