闭包的Lua中

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了闭包的Lua中相关的知识,希望对你有一定的参考价值。

参考技术A

当一个函数内部嵌套另一个函数定义时,内部的函数体可以访问外部的函数的局部变量,这种特征在lua中我们称作词法定界。虽然这看起来很清楚,事实并非如此,词法定界加上第一类函数在编程语言里是一个功能强大的概念,很少语言提供这种支持。
下面看一个简单的例子,假定有一个学生姓名的列表和一个学生名和成绩对应的表;想根据学生的成绩从高到低对学生进行排序, names = Peter,Paul,Mary
grades = Mary = 10,Paul = 7,Peter = 8
table.sort(names,function (n1,n2)
return grades[n1] > grades[n2] -- compare the grades
end) 假定创建一个函数实现此功能:
function sortbygrade (names,grades)
table.sort(names,function (n1,n2)
return grades[n1] > grades[n2] --compare the grades
end) 例子中包含在sortbygrade函数内部的sort中的匿名函数可以访问sortbygrade的参数grades,在匿名函数内部grades不是全局变量也不是局部变量,我们称作外部的局部变量(external local variable)或者upvalue。(upvalue意思有些误导,然而在Lua中他的存在有历史的根源,还有他比起external local variable简短)。
看下面的代码:
function newCounter()
local i = 0
return function() -- anonymous function
i = i + 1
return i
end
end
c1 = newCounter()
print(c1()) --> 1
print(c1()) --> 2
匿名函数使用upvalue i保存他的计数,当我们调用匿名函数的时候i已经超出了作用范围,因为创建i的函数newCounter已经返回了。然而Lua用闭包的思想正确处理了这种情况。简单的说,闭包是一个函数以及它的upvalues。如果我们再次调用newCounter,将创建一个新的局部变量i,因此我们得到了一个作用在新的变量i上的新闭包。
c2 = newCounter()
print(c2()) --> 1
print(c1()) --> 3
print(c2()) --> 2
c1、c2是建立在同一个函数上,但作用在同一个局部变量的不同实例上的两个不同的闭包。
技术上来讲,闭包指值而不是指函数,函数仅仅是闭包的一个原型声明;尽管如此,在不会导致混淆的情况下我们继续使用术语函数代指闭包。
闭包在上下文环境中提供很有用的功能,如前面我们见到的可以作为高级函数(sort)的参数;作为函数嵌套的函数(newCounter)。这一机制使得我们可以在Lua的函数世界里组合出奇幻的编程技术。闭包也可用在回调函数中,比如在GUI环境中你需要创建一系列button,但用户按下button时回调函数被调用,可能不同的按钮被按下时需要处理的任务有点区别。具体来讲,一个十进制计算器需要10个相似的按钮,每个按钮对应一个数字,可以使用下面的函数创建他们:
function digitButton (digit)
return Button label = digit,
action = function ()
add_to_display(digit)
end

end
这个例子中我们假定Button是一个用来创建新按钮的工具,label是按钮的标签,action是按钮被按下时调用的回调函数。(实际上是一个闭包,因为他访问upvalue digit)。digitButton完成任务返回后,局部变量digit超出范围,回调函数仍然可以被调用并且可以访问局部变量digit。
闭包在完全不同的上下文中也是很有用途的。因为函数被存储在普通的变量内我们可以很方便的重定义或者预定义函数。通常当你需要原始函数有一个新的实现时可以重定义函数。例如你可以重定义sin使其接受一个度数而不是弧度作为参数:
oldSin = math.sin
math.sin = function (x)
return oldSin(x*math.pi/180)
end
更清楚的方式:
do
local oldSin = math.sin
local k = math.pi/180
math.sin = function (x)
return oldSin(x*k)
end
end
这样我们把原始版本放在一个局部变量内,访问sin的唯一方式是通过新版本的函数。
利用同样的特征我们可以创建一个安全的环境(也称作沙箱,和java里的沙箱一样),当我们运行一段不信任的代码(比如我们运行网络服务器上获取的代码)时安全的环境是需要的,比如我们可以使用闭包重定义io库的open函数来限制程序打开的文件。
do
local oldOpen = io.open
io.open = function (filename,mode)
if access_OK(filename,mode) then
return oldOpen(filename,mode)
else
return nil,access denied
end
end Scheme中的闭包
其他编程的语言主要采用的是闭包的第二种意义(一个与闭包毫不相干的概念):闭包也算一种为表示带有自由变量的过程而用的实现技术。但Scheme的术语“闭包”来自抽象代数。在抽象代数里,一集元素称为在某个运算(操作)之下封闭,如果将该运算应用于这一集合中的元素,产生出的仍然是该集合里的元素。
用Scheme的序对举例,为了实现数据抽象,Scheme提供了一种称为序对的复合结构。这种结构可以通过基本过程cons构造出来。过程cons取两个参数,返回一个包含这两个参数作为其成分的复合数据对象。请注意,一个序对也算一个数据对象。进一步说,还可以用cons去构造那种其元素本身就是序对的序对,并继续这样做下去。
(define x (cons 1 2)) //构造一个x序对,有1,2组成
(define y (cons 3 4))
(define z (cons x y))
Scheme可以建立元素本身也算序对的序对,这就是表结构得以作为一种表示工具的根本基础。我们将这种能力称为cons的闭包性质。一般说,某种组合数据对象的操作满足闭包性质,那就是说,通过它组合起数据对象得到的结果本身还可以通过同样的操作再进行组合。闭包性质是任何一种组合功能的威力的关键要素,因为它使我们能够建立起层次性结构,这种结构由一些部分构成,而其中的各个部分又是由它们的部分构成,并且可以如此继续下去。

Javascript 和 Lua 闭包的区别

【中文标题】Javascript 和 Lua 闭包的区别【英文标题】:Difference in Closures between Javascript and Lua 【发布时间】:2013-10-14 17:33:21 【问题描述】:

为什么这两段看似相同的代码在 Javascript 和 Lua 中表现不同?

卢阿:

function main()
    local printFunctions=
    local i,j
    for i=1,10 do
        local printi = function()
            print(i)
        end
        printFunctions[i]=printi
    end
    for j=1,10 do
        printFunctions[j]()
    end
end
main()

Javascript:

function main()

    var printFunctions=[]
    var i,j;
    for(i=0;i<10;i++)
    
        var printi = function()
        
            console.log(i);
        
        printFunctions[i]=printi;
    
    for(j=0;j<10;j++)
    
        printFunctions[j]();
    

main()

Lua 中的示例打印0 1 2 3 4 5 6 7 8 9,但 Javascript 中的示例打印10 10 10 10 10 10 10 10 10 10。谁能解释导致这种情况发生的 Javascript 和 Lua 中的闭包之间的区别?我来自 Javascript 背景,所以请关注 Lua 方面。

我试图在 my blog 上解释这一点,但我不确定我的解释是否正确,因此我们将不胜感激。

编辑

谢谢大家,现在我明白了。这个稍加修改的 Lua 代码版本按预期打印 10,10,10,10,10,10,10,10,10,10

function main()
    local printFunctions=
    local i,j,k
    for i=1,10 do
        k=i
        local printi = function()
            print(k)
        end
        printFunctions[i]=printi
    end
    for j=1,10 do
        printFunctions[j]()
    end
end

main()

【问题讨论】:

【参考方案1】:

就这么简单:

Lua local 变量的范围仅限于最近的 do-end 块,而使用 var 声明的 JavaScript 变量的范围仅限于最近的函数边界。闭包通过将它们放置在函数中自己的作用域中来克服这一点,从而解决作用域问题。

关于local i, j 在外部范围内的问题,Lua 中的 for 语句会创建在块范围内使用的计数器的范围,即使外部范围内有变量声明。文档说(reference):

循环变量 v 是循环的局部变量;你不能在 for 结束或被破坏后使用它的值。如果您需要此值,请在中断或退出循环之前将其分配给另一个变量。

这意味着 var 的初始化在 for 循环范围内是本地的,因此将 local i, j 放在外部范围内无效。这可以在文档中给出的 Lua 的 for 语句中看到:

do
    local var, limit, step = tonumber(e1), tonumber(e2), tonumber(e3)
    if not (var and limit and step) then error() end
    while (step > 0 and var <= limit) or (step <= 0 and var >= limit) do
        local v = var
        block
        var = var + step
    end
end

然而,JavaScript 的 for 语句差异很大 (reference):

IterationStatement : for (var VariableDeclarationListNoIn ; Expressionopt ; Expressionopt ) 语句

因为for循环的声明和任何普通的变量声明都是一样的,所以相当于把它放在了循环之外,与Lua for循环的工作方式大不相同。下一版本的 ECMAScript (ES6) 计划引入 let 关键字,它在 for 循环中的含义与 Lua 的 for 循环的工作方式类似:

for (let i = 0; i < 10; ++i) setTimeout(function ()  console.log(i); , 9); // 0,1,2,3,4,5,6,7,8,9
for (var i = 0; i < 10; ++i) setTimeout(function ()  console.log(i); , 9); // 10,10,10,10,10,10,10,10,10,10

【讨论】:

但在这两种情况下,ij 都声明在最外层范围内(在 main 函数内),因此它们在两种语言中应该具有相同的范围。 @FlightOdyssey:对于 Lua 案例,情况并非如此。 Lua for 循环总是声明一个新变量,因此循环中的变量每次迭代都是一个新变量。你声明的local i 最终会被隐藏和忽略(如果你删除它,结果应该是一样的)。 @FlightOdyssey 一个好的 Lua IDE 会在大纲中显示变量阴影,在编辑器中显示未使用的变量和未使用的赋值以及在调试器中具有相同名称的多个局部变量(由于阴影)。 (抱歉,我不能向您推荐我建造的那个。)

以上是关于闭包的Lua中的主要内容,如果未能解决你的问题,请参考以下文章

lua学习之闭包实现原理

深入理解Lua的闭包一:概念应用和实现原理

Lua中的闭包

Unity游戏开发 | 浅谈Lua和C#中的闭包

lua的闭包

lua语言:闭包模式匹配日期编译模块