Lua编程:深入理解Upvalue和闭包

一、UpValue和闭包的概念

Lua 语言中,函数是严格遵循词法定界的第一类值。Lua 中,函数是第一类类型值,这意味着定义函数和其他普通类型是一样的,区别在于函数对应的数据值是对应的函数体的指令罢了。一个程序可以将某个函数保存到变量中(全局变量和局部变量均可)或表中,也可以将某个函数作为参数传递给其他函数,还可以将某个函数作为其他函数的返回值返回。

“词法定界”这个特性,使得可以在函数内再定义内嵌的函数,即函数可以访问包含其自身的外部函数中的变量。如果函数f2在函数f1中,那么将f2称为f1的内嵌函数,而于f1称为f2的外包函数,内嵌函数可以访问其外包函数中的所有局部变量,这种特性称为词法作用域,而这些局部变量就称为该内嵌函数的外部局部变量,或者常说的UpValue(也成为非局部变量)。

闭包就是函数加上它所需访问的UpValue,前者涉及代码,而UpValue则与函数环境相关。

示例:

function newCounter()
	local i=0
	return function()
		i=i+1
		return i
	end
end

n1=newCounter()
print(n1())
print(n1())

n2=newCounter()
print(n2())
print(n2())

执行结果:

1
2

1
2

函数newCounter每次返回一个匿名函数,每次调用时将其所引用的UpValue 递增;注意到n1和n2的函数体都是一样的,但是环境( UpValue )却不相同,这也是当n1输出结果是2时,n2新创建出来的函数输出还是1的原因。

二、函数是第一类值

体验下第一类值的含义:

> a = {p = print} --a.p 指向print 函数
> a.p ("hello world")
hello world
> print=math.sin -- print现在指向sin函数
> a.p(print(1)) 
0.8414709848079 
> math.sin = a.p -- sin 现在指向print 函数
> math.sin (10 , 20)
10 20 

Lua语言中常见的函数定义方式:

function first(x) return x*x end
--等价于
first=function(x) return x*x end

赋值语句右边的表达式就是函数构造器,与表构造器 {} 相似,函数定义实际上就是创建类型为 function 的值并把它赋值给一个变量的语句。

Lua 语言中,所有的函数都是匿名的;像其他所有的值一样,函数并没有名字。当讨论函数名时 ,比如print,实际上指的是保存该函数的变量,虽然通常会把函数赋值给全局变量,从而看似给函数起了一个名字, 但在很多场景下仍然会保留函数的匿名性。

三、非全局函数

由于函数是一种“第一类值”,因此一个显而易见的结果就是:函数不仅可以被存储在全局变量中,还可以被存储在表字段和局部变量中。大部分Lua语言的库就采用了这种机制(例如 io.read和math.sin )。

Lib={}
Lib.add=function(x,y) return x+y end
lib.sub=function(x,y) return x-y end
--等价于使用表构造器
Lib={
	add=function(x,y) return x+y end
	sub=function(x,y) return x-y end
}
--或者这样
Lib={}
function Lib.add(x,y) return x+y end
function Lib.sub(x,y) return x-y end

当把一个函数存储到局部变量时,就得到了一个局部函数,即一个被限定在指定作用域中使用的函数。局部函数对于包( package )而言尤其有用。词法定界保证了程序段中的其他函数可以使用这些局部函数。

值得注意的是,在定义局部递归函数时,有一点比较容易出错:

local fact=function(n)
	if n==0 then return 1
	else return n*fact(n-1)		--有问题
	end
end

上述代码中,由于局部fact还没创建,直接调用时将无法使用局部的fact,Lua解析器会去寻找全局的fact函数来调用。可以通过先定义局部变量再定义函数的方式来解决这个问题:

local fact
fact=function(n)
	if n==0 then return 1
	else return n*fact(n-1)		--有问题
	end
end

尽管在定义函数时,这个局部变量的值尚未确定,但到了执行函数时, fact 肯定已经有了正确的赋值。
当然,也可以使用如下的方式解决上述问题:

local function fact (n) body end

Lua解析器会把它展开为:

local fact
fact=function (n) body end

但是,这个技巧对于间接递归函数是无效的;在间接递归的情况下,必须使用与明确的前向声明等价的形式。

四、词法定界

当编写一个被其它函数A包含的函数B时,被包含的函数B可以访问包含其的函数A的所有局部变量,将这种特性称为词法定界。词法定界外加嵌套的第一类值函数可以为编程语言提供强大的功能,但很多编程语言并不支持将这两者组合使用。

UpValue之所以有趣,函数作为第一类值,能够逃逸出它们变量的原始定界范围。可以看看如下的代码:

function newCounter()
	local i=0
	return function()
		i=i+1
		return i
	end
end

n1=newCounter()
print(n1())
print(n1())

执行结果:

1
2

匿名函数访问了一个非局部变量( i)并将其当作计数器;然而,由于创建变量的函数(newCounter)己经返回,因此当调用匿名函数时,变量 count 似乎已经超出了作用范围。但其实不然,由于闭包概念的存在,Lua 语言能够正确地应对这种情况。简单地说,一个闭包就是一个函数外加能够使该函数正确访问非局部变量所需的其他机制

闭包在许多场合中均是一种有价值的工具。比如作为sort 这样的高阶函数参数时就非常有用。同样,闭包对于那些创建了其他函数的函数也很有用。另外,闭包对于回调 callback 函数来说也很有用。

闭包用来创建安全的运行时环境,即所谓的沙盒。当执行一些诸如从远程服务器上下载到的未受信任代码时,安全的运行时环境非常重要。比如,可以通过使用闭包重定义函数 io.open 来限制一个程序能够访问的文件:

do
	local oldOpen=io.open
	local access_ok=function(filename,mode)
		--check
	end
	io.open=function(filename,mode)
		if access_ok(filename,mode) the
			return oldOpen(filename,mode)
		else
			return nil,"access fail"
		end
	end
end

五、函数式编程示例

目标是开发一个用来表示几何区域的系统,其中区域即为点的集合,能够利用该系统表示各种各样的图形 ,同时可以通过多种方式(旋转 、变换、并集等)组合和修改这些图形。鉴于一个几何区域就是点的集合,因此可以通过特征数来表示一个区域, 即可以提供一个点(作为参数)并根据点是否属于指定区域而返回真或假的函数来表示一个区域。

print("Hello World")
-- 圆形
function disk(cx,cy,r)
    return function(x,y)
        return (x-cx)^2 + (y-cy)^2<=r^2
    end
end
-- 矩形
function rect(left,right,bottom,up)
    return function(x,y)
        return left<=x and x<=right and bottom<=y and y<=up
    end
end
-- 补集
function complement(r)
    return function(x,y)
        return not r(x,y)
    end
end
-- 并集
function union(r1,r2)
    return function(x,y)
        return r1(x,y) or r2(x,y)
    end
end

-- 交集
function intersection(r1,r2)
    return function(x,y)
        return r1(x,y) and r2(x,y)
    end
end

-- 差集
function difference(r1,r2)
    return function(x,y)
        return r1(x,y) and not r2(x,y)
    end
end

--平移
function translate(r,dx,dy)
    return function (x,y)
        return r(x-dx,y-dy)
    end
end

-- PBM 文件中绘制区域
function plot(r,M,N)
    io.write("P1\n",M," ",N,"\n")   -- 头
    for i=1,N do                    -- 行
        local y=(N-i*2)/N
        for j=1,M do                -- 列
            local x=(j*2-M)/M
            io.write(r(x,y) and "1" or "0")
        end
        io.write("\n")
    end
end
--绘制了一个南半球所能看到的娥眉月
c1=disk(0,0,1)
plot(difference(c1,translate(c1,0.3,0)),500,500)

PBM 文件的结构很简单,PBM文件的文本形式以字符串“ P1 开头,接下来的一行是图片的宽和高(以像素为单位),然后是对应每一个像素、由1和0组成的数字序列,最后是 EOF。
在这里插入图片描述

六、总结

  1. 闭包的表现,函数内部可以访问函数外部的变量。
  2. lua文件是一个匿名函数,lua 内部函数可以访问文件中函数体外的变量。
  3. 闭包的实现,C 函数以及绑定在 C 函数上的上值。
  4. lua语言与其他语言差异:没有入口函数;索引从 1 开始;闭包;可以有多返回值;函数是第一类型;尾递归,不占用栈空间;条件表达式是nil 或者 false 为假,非 nil 为真;支持多元运算;非运算符是 ~ 而不是 !,所以不等于为 ~=。

在这里插入图片描述

  • 5
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Lion Long

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值