Lua闭包

最后更新于 2023-02-21 660 次阅读


upvalue

如果函数f2定义在函数f1中,那么f2为f1的内嵌函数,f1为f2的外包函数,外包和内嵌都具有传递性,即f2的内嵌必然是f1的内嵌,而f1的外包也一定是f2的外包。

内嵌函数可以访问外包函数已经创建的局部变量,而这些局部变量则称为该内嵌函数的外部局部变量(或者upvalue)

function f1(n)
	-- 函数参数也是局部变量
	local function f2()
		print(n) -- 引用外包函数的局部变量
	end
	return f2
end
g1 = f1(1979)
g1() -- 打印出1979
g2 = f1(500)
g2() -- 打印出500

当执行完g1=f1(1979)后,局部变量n的生命本该结束,但因为它已经成了内嵌函数f2的upvalue,f2又被赋给了变量g1,所以它仍然能以某种形式继续“存活”下来,从而令g1()打印出正确的值

闭包

在Lua中,闭包(closure)是由一个函数和该函数会访问到的非局部变量(或者是upvalue)组成的,其中非局部变量(non-local variable)是指不是在局部作用范围内定义的一个变量,但同时又不是一个全局变量,主要应用在嵌套函数和匿名函数里,因此若一个闭包没有会访问的非局部变量,那么它就是通常说的函数。也就是说,在Lua中,函数是闭包一种特殊情况。另外在Lua的C API中,所有关于Lua中的函数的核心API都是以closure(而非function)来命名的,也可视为这一观点的延续。在Lua中,函数是一种第一类型值(First-Class Value),它们具有特定的词法域(Lexical Scoping)。

第一类型值表示函数与其他传统类型的值(例如数字和字符串类型)具有相同的权利即函数可以存储在变量或table中,可以作为实参传递给其他函数,还可以作为其他函数的返回值,可以在运行期间被创建。在Lua中,函数与所有其他的值是一样都是匿名的,即他们没有名称。当讨论一个函数时(例如print),实质上在讨论一个持有某个函数的变量

在运行时,每当Lua执行一个形如function...end这样的函数时,它就会创建一个新的数据对象,其中包含了相应函数原型的引用、环境(用来查找全局变量的表)的引用以及一个由所有upvalue引用组成的数组,而这个数据对象就称为闭包。

由此可见,函数是编译期概念,是静态的,而闭包是运行期概念,是动态的。g1和g2的值严格来说不是函数而是闭包,并且是两个不相同的闭包,而这两个闭包有各自的upvalue值。

function f1(n)
	local function f2()
		print(n)
	end
	n = n + 10
	return f2
end

g1 = f1(1979)
g1() -- 打印出1989

g1()打印出来的是1989,原因是打印的是upvalue的值。

upvalue实际是局部变量,而局部变量是保存在函数堆栈框架上的,所以只要upvalue还没有离开自己的作用域,它就一直生存在函数堆栈上。这种情况下,闭包将通过指向堆栈上的upvalue的引用来访问它们,一旦upvalue即将离开自己的作用域,在从堆栈上消除之前,闭包就会为它分配空间并保存当前的值,以后便可通过指向新分配空间的引用来访问该upvalue

当执行到f1(1979)的n=n+10时,闭包已经创建了,但是变量n并没有离开作用域,所以闭包仍然引用堆栈上的n,当return f2完成时,n即将结束生命,此时闭包便将变量n(已经是1989了)复制到自己管理的空间中以便将来访问。 

upvalue和闭包数据共享

upvalue还可以为闭包之间提供一种数据共享的机制。

单重内嵌函数的闭包 (函数创建的闭包)

一个函数创建的闭包共享一份upvalue。

local function g1(n)
    local function g2()
        print(n)
    end

    local function g3()
        n = n + 10
    end
    return g2,g3
end

x1,x2 = g1(123)
x1() //123
x2()
x1() //133
x2()
x1() //143

f1,f2这两个闭包的原型分别是Create中的内嵌函数foo1和foo2,而foo1和foo2引用的upvalue是同一个,即Create的局部变量n。执行完Create调用后,闭包会把堆栈上n的值复制出来,那么是否f1和f2就分别拥有一个n的拷贝呢?其实不然,当Lua发现两个闭包的upvalue指向的是当前堆栈上的相同变量时,会聪明地只生成一个拷贝,然后让这两个闭包共享该拷贝

这样任一个闭包对该upvalue进行修改都会被另一个探知。上述例子很清楚地说明了这点:每次调用f2都将upvalue的值增加了10,随后f1将更新后的值打印出来。upvalue的这种语义很有价值,它使得闭包之间可以不依赖全局变量进行通讯,从而使代码的可靠性大大提高。

多重内嵌函数的闭包(闭包创建的闭包)

同一闭包创建的其他的闭包共享一份upvalue。

内层闭包在创建之时其需要的变量就已经不在堆栈上,而是引用更外层外包函数的局部变量(实际上是upvalue)。

local function C(n)
    local function c1()
        local function cc1()
            print(n)
        end

        local function cc2()
            n = n + 10
        end

        return cc1,cc2
    end
    return c1
end

t = C(1883)

tt1,tt2 = t()
tt1() --1883
tt2()
tt1() --1893

kk1,kk2 = t()
kk1() --1893
kk2()
kk1() --1903
tt2()
tt1() --1903

tt1和tt2与kk1和kk2共享同一个upvalue。因为tt1和tt2与kk1和kk2都是同一个闭包t创建的所以它们引用的upvalue  (变量n)实际也是同一个变量,而它们的upvalue引用都会指向同一个地方。

闭包的应用

在许多场合中闭包都是一种很有价值的工具,主要有以下几个方面:

  • 作为高阶函数的参数,比如像table.sort函数的参数。
  • 创建其他的函数的函数,即函数返回一个闭包
  • 闭包对于回调函数也非常有用。典型的例子就是界面上按钮的回调函数,这些函数代码逻辑可能是一模一样,只是回调函数参数不一样而已,即upvalue的值不一样而已。
  • 创建一个安全的运行环境,即所谓的沙盒(sandbox)。当执行一些未受信任的代码时就需要一个安全的运行环境。比如要限制一个程序访问文件的话,只需要使用闭包来重定义函数io.open就可以了
  • 实现迭代器。所谓迭代器就是一种可以遍历一种集合中所谓元素的机制。每个迭代器都需要在每次成功调用之间保持一些状态,这样才能知道它所在的位置及如何进到下一个位置。闭包刚好适合这种场景。