为什么 Lua 嵌套函数 B 是否声明为 local 会改变执行结果?
结论
这里需要使用 local function B(),让每次调用 A 时创建的闭包都能通过名称 B 引用自身。去掉 local 后,每次执行 A 都会覆盖同一个全局 B,后续递归调用传递的函数也会随之变化。
这就是题目中结果不同的原因:使用局部 B 得到 0,使用全局 B 得到 1。闭包仍能读取正确的 k 和参数,因为这些变量由闭包捕获;发生变化的是函数体内名称 B 的值。
两种声明实际绑定了什么
这段代码没有外层局部变量 B,因此:
function B()
-- ...
end
相当于执行一次赋值:
B = function()
-- ...
end
这条语句在 A 内部。每次运行 A,程序都会创建一个闭包,并将它赋给全局 B。即使这次调用的 k <= 0,程序也会先执行函数定义,再进入条件分支。
局部函数声明的绑定关系则是:
local B
B = function()
-- 此处的 B 引用上面声明的局部变量
end
每次调用 A 都有自己的局部 B。闭包可以通过 B 引用自身,也能捕获这一层 A 的 k、x1、x2 等变量。
因此,原代码中的这一行会产生不同效果:
return A(k, B, x1, x2, x3, x4)
使用局部 B 时,传入的是当前闭包自身;使用全局 B 时,传入的是全局变量 B 此刻保存的函数,它可能已经被更深一层 A 覆盖。
为什么 x4 调用了旧闭包,日志却显示另一个 B
函数对象和保存函数的变量是两回事。
把某个 B 作为参数传给 A 后,x4 可以一直保存那个函数对象。之后覆盖全局 B,不会修改 x4 保存的值,也不会改变旧闭包捕获的 k 和参数。
但旧闭包执行下面这句时:
print(" B" .. fs(B) .. is(k) .. fs(x1) .. fs(x2) .. fs(x3) .. fs(x4)
.. fs(x5), _G, _ENV)
fs(B) 会重新读取名称 B。使用全局声明时,它读到的是最新的全局 B,不一定是当前正在执行的闭包。
题目给出的跟踪显示了这一过程:
A f001 0 f011 f010 f009 f008 f007
B f012 3 f007 f002 f003 f004 f005
这一层 A 的 x4 是 f008。执行 x4() 时,调用的仍然是 f008,所以它能读取自己捕获的参数。
进入 k = 0 的 A 时,程序又创建了一个 B,覆盖了全局 B。下一行日志中的 f012 来自 fs(B),表示新闭包,而不是正在执行的 f008。
日志中的 k 为 3 也符合这一过程:f008 创建于 k = 4 的那层 A。它此前执行过一次 k = k - 1,捕获的 k 已经变成了 3。
日志之后的递归调用同样会读取全局 B,因此会把新闭包传给 A,改变算法的行为。
如何修改代码
将 B 声明为局部函数即可。去掉辅助打印后,核心代码可以写成:
local function A(k, x1, x2, x3, x4, x5)
local function B()
k = k - 1
return A(k, B, x1, x2, x3, x4)
end
if k <= 0 then
return x4() + x5()
end
return B()
end
local function K(n)
local function Kn()
return n
end
return Kn
end
print(A(5, K(1), K(-1), K(-1), K(1), K(0)))
这里也将 Kn 改成了局部函数,避免每次调用 K 都覆盖全局 Kn。原始 Kn 的函数体没有通过名称 Kn 引用自身,K 又会立即返回刚创建的函数,所以 Kn 不是本次结果差异的原因。
两个容易混淆的细节
第一次调用 A 前,全局 B 为 nil,因为 A 内部的赋值语句还没有执行。全局作用域决定名称如何解析,赋值要等程序运行到对应位置才会发生。
使用 local function B 后,全局 B 仍为 nil,因为程序没有给全局 B 赋值。局部 B 可能通过 x1、x4 等参数继续被其他闭包引用,不会仅因创建它的那次 A 调用返回就立即失效。如果原先已有一个全局 B,局部声明也不会将它清空。
另外,不要随意把递归函数改写为 local B = function() ... end。如果没有预先声明局部 B,新局部变量的作用域不包含它自己的初始化表达式,函数体中的 B 可能解析到外层变量。这里直接使用 local function B() 最清楚。
备注:内容仅供参考。
