8.1 局部变量和代码块
Lua 语言中的变量在默认情况下是全局变量 ,所有的局部变量在使用前必须声明 。 与全局变量不同,局部变量的生效范围仅限于声明它的代码块。一个代码块( block )是一个控制结构的主体,或是一个函数的主体,或是一个代码段(即变量被声明时所在的文件或字符串):
x = 10
local i = 1 -- 对于代码段来说是局部的
while i <= x do
local x = i*2 -- 对于循环体来说是局部的
print(x)
i = i + 1
end
if i > 20 then
local x -- 对于then来说是局部的
x = 20
print(x + 2) -- 如果测试成功输出22
else
print(x) --> 10 (全局的)
end
print(x) --> 10 (全局的)
请注意,上述示例在交互模式中不能正常运行。 因为在交互模式中,每一行代码就是一个代码段(除非不是一条完整的命令)。 一旦输入示例的第二行, Lua 语言解释器就会直接运行它并在下一行开始一个新的代码段。 这样,局部( local )的声明就超出了原来的作用范围 。 解决这个问题的一种方式是显式地声明整个代码块, 即将它放入一对 do-end 中 。一旦输入了do,命令就只会在遇到匹配的 end 时才结束,这样 Lua 语言解释器就不会单独执行每一行的命令。
当需要更好地控制某些局部变量的生效范围时, do 程序块也同样有用:
local x1, x2
do
local a2 = 2*a
local d = (b^2 - 4*a*c)^(1/2)
x1 = (-b + d)/a2
x2 = (-b - d)/a2
end -- 'a2'和'd'的范围在此结束
print(x1, x2) -- 'x1'和'x2'仍在范围内
尽可能地使用局部变量是一种良好的编程风格。 首先,局部变量可以避免由于不必要的命名而造成全局变量的混乱;其次,局部变量还能避免同一程序中不同代码部分中的命名冲突;再次,访问局部变量比访问全局变量更快;最后,局部变量会随着其作用域的结束而消失,从而使得垃圾收集器能够将其释放。
鉴于局部变量优于全局变量,有些人就认为 Lua 语言应该把变量默认视为局部的。 然而,把变量默认视为局部的也有一系列的问题(例如非局部变量的访问问题)。一个更好的解决办法并不是把变量默认视为局部变量,而是在使用变量前必须先声明 。 Lua 语言的发行版中有一个用于全局变量检查的模块 strict.lua , 如果试图在一个函数中对不存在的全局变量赋值或者使用不存在的全局变量 ,将会抛出异常。 这在开发 Lua 语言代码时是一个良好的习惯。
局部变量的声明可以包含初始值,其赋值规则与常见的多重赋值一样:多余的值被丢弃 ,多余的变量被赋值为 nil 。 如果一个声明中没有赋初值,则变量会被初始化为 nil:
local a, b = 1, 10
if a < b then
print(a) --> 1
local a -- '= nil'是隐式的
print(a) --> nil
end
print(a, b) --> 1 10
Lua 语言中有一种常见的用法:
local foo = foo
这段代码声明了一个局部变量 foo 然后用全局变量 foo 对其赋初值(局部变量 foo 只有在声明之后才能被访问)。 这个用法在需要提高对 foo 的访问速度时很有用。 当其他函数改变了全局变量 foo 的值,而代码段又需要保留 foo 的原始值时,这个用法也很有用,尤其是在进行运行时动态替换时。 即使其他代码把 print 动态替换成了其他函数,在 local print =print 语句之前的所有代码使用的还都是原先的 print 函数。
有些人认为在代码块的中间位置声明变量是一个不好的习惯,实际上恰恰相反:我们很少会在不赋初值的情况下声明变量 ,在需要时才声明变量可以避免漏掉初始化这个变量。 此外,通过缩小变量的作用域还有助于提高代码的可读性。
8.2 控制结构
Lua 语言提供了一组精简且常用的控制结构,包括用于条件执行的 if 以及用于循环的 while 、 repeat 和 for 。 所有的控制结构语法上都有一个显式的终结符: end用于终结 if 、 for 及 while 结构, until 用于终结 repeat 结构 。
控制结构的条件表达式的结果可以是任何值。 请记住,Lua 语言将所有不是 false 和 nil 的值当作真(特别地, Lua 语言将 0 和空字符串也当作真)。
8.2.1 if then else
if 语句先测试其条件,并根据条件是否满足执行相应的 then 部分或 else 部分。 else 部分
是可选的 。
if a < 0 then a = 0 end
if a < b then return a else return b end
if line > MAXLINES then
showpage()
line = 0
end
如果要编写嵌套的 if 语句,可以使用 elseif 。 它类似于在 else 后面紧跟一个 if,但可以避免重复使用 end:
if op == "+" then
r = a + b
elseif op == "-" then
r = a - b
elseif op == "*" then
r = a * b
elseif op == "/" then
r = a / b
else
error("invalid operation")
end
由于 Lua 语言不支持 switch 语句,所以这种一连串的 else-if 语句比较常见。
8.2.2 while
顾名思义,当条件为真时 while 循环会重复执行其循环体。 Lua 语言先测试 while 语句的条件,若条件为假则循环结束;否则, Lua 会执行循环体并不断地重复这个过程。
local i = 1
while a[i] do
print(a[i])
i = i + 1
end
8.2.3 repeat
顾名思义, repeat-until 语句会重复执行其循环体直到条件为真时结束。 由于条件测试在循环体之后执行,所以循环体至少会执行一次。
-- 输出第一个非空的行
local line
repeat
line = io.read()
until line ~= ""
print(line)
和大多数其他编程语言不同,在 Lua 语言中,循环体内声明的局部变量的作用域包括测试条件:
-- 使用Newton-Raphson计算'x'的平方根
local sqr = x / 2
repeat
sqr = (sqr + x/sqr) / 2
local error = math.abs(sqr^2 - x)
until error < x/10000 -- 局部变量error此时依然可见
8.2.4 数值型 for
for 语句有两种形式: 数值型 for 和泛型 for 。
数值型 for 的语法如下:
for var = exp1, exp2, exp3 do
something
end
在这种循环中, var 的值从 exp1 变化到 exp2 之前的每次循环会执行 something ,并在每次循环结束后将步长 exp3 增加到 var 上。 第三个表达式 exp3 是可选的,若不存在, Lua语言会默认步长值为 1 。 如果不想给循环设置上限,可以使用常量 math.huge:
for i = 1, math.huge do
if (0.3*i^3 - 20*i^2 - 500 >= 0) then
print(i)
break
end
end
为了更好地使用 for 循环,还需要了解一些细节。首先,在循环开始前, 三个表达式都会运行一次;其次,控制变量是被 for 语句自动声明的局部变量 ,且其作用范围仅限于循环体内 。一种典型的错误是认为控制变量在循环结束后仍然存在:
for i = 1, 10 do print(i) end
max = i -- 可能会出错!此处的'i'是全局的
如果需要在循环结束后使用控制变量的值(通常在中断循环时),则必须将控制变量的值保存到另一个变量中:
-- 在一个列表中寻找一个值
local found = nil
for i = 1, #a do
if a[i] < 0 then
found = i -- 保存'i'的值
break
end
end
print(found)
最后,不要改变控制变量的值,随意改变控制变量的值可能产生不可预知的结果。 如果要在循环正常结束前停止 for 循环,那么可以参考上面的例子,使用 break 语句。
8.2.5 泛型 for
泛型 for 遍历迭代函数返回的所有值,例如我们已经在很多示例中看到过的 pairs 、ipairs 和 io.lines 等。 虽然泛型 for 看似简单,但它的功能非常强大。 使用恰当的迭代器可以在保证代码可读性的情况下遍历几乎所有的数据结构。
当然,我们也可以自己编写迭代器。 尽管泛型 for 的使用很简单 , 但编写迭代函数却有不少细节需要注意。
与数值型 for 不同,泛型 for 可以使用多个变量 , 这些变量在每次循环时都会更新。 当第一个变量变为 nil 时 ,循环终止。 像数值型 for 一样,控制变量是循环体中的局部变量 ,我们也不应该在循环中改变其值。
8.3 break 、 return 和 goto
break 和 return 语句用于从当前的循环结构中跳出, goto 语句则允许跳转到函数中的几乎任何地方。
我们可以使用 break 语句结束循环,该语句会中断包含它的内层循环(例如 for 、repeat或者 while );该语句不能在循环外使用 。 break 中断后,程序会紧接着被中断的循环继续执行。
return 语句用于返回函数的执行结果或简单地结束函数的运行。 所有函数的最后都有一个隐含的 return , 因此我们不需要在每一个没有返还值的函数最后书写 return 语句 。
按照语法, return 只能是代码块中的最后一句:换句话说,它只能是代码块的最后一句,或者是 end 、 else 和 until 之前的最后一句。 例如,在下面的例子中, return 是 then 代码块的最后一句 :
local i = 1
while a[i] do
if a[i] == v then return i end
i = i + 1
end
通常,这些地方正是使用 return 的典型位置, return 之后的语句不会被执行。 不过,有时在代码块中间使用 return 也是很有用的 。 例如,在调试时我们可能不想让某个函数执行。 在这种情况下,可以显式地使用一个包含 return 的 do:
function foo()
return --<< SYNTAX ERROR
-- 'return'是下一个代码块的最后一句
do return end -- OK
other statements
end
goto 语句用于将当前程序跳转到相应的标签处继续执行。 goto 语句一直以来备受争议,至今仍有很多人认为它们不利于程序开发并且应该在编程语言中禁止。不过尽管如此,仍有很多语言出于很多原因保留了 goto 语句 。goto 语句有很强大的功能,只要足够细心,我们就能够利用它来提高代码质量。
在 Lua 语言中 , goto 语句的语法非常传统, 即保留字 goto 后面紧跟着标签名,标签名可以是任意有效的标识符。 标签的语法稍微有点复杂:标签名称前后各紧跟两个冒号,形如 ::name:: 。 这个复杂的语法是有意而为的,主要是为了在程序中醒目地突出这些标签 。
在使用 goto 跳转时·,Lua 语言设置了一些限制条件。 首先,标签遵循常见的可见性规则,因此不能直接跳转到一个代码块中的标签(因为代码块中的标签对外不可见 ) 。 其次,goto
不能跳转到函数外(注意第一条规则已经排除了跳转进一个函数的可能性)。最后,goto 不能跳转到局部变量的作用域。
关于 goto 语句典型且正确的使用方式,请参考其他一些编程语言中存在但 Lua 语言中不存在的代码结构,例如 continue 、多级 break 、多级 continue 、redo 和局部错误处理等。continue语句仅仅相当于一个跳转到位于循环体最后位置处标签的 goto 语句,而 redo 语句则相当于跳转到代码块开始位置的 goto 语句:
while some_condition do
::redo::
if some_other_condition then goto continue
else if yet_another_condition then goto redo
end
some code
::continue::
end
Lua 语言规范中一个很有用的细节是,局部变量的作用域终止于声明变量的代码块中的最后一个有效 ( non-void )语句处 ,标签被认为是无效( void ) 语句。 下列代码展示了这个实用的细节:
while some_condition do
if some_other_condition then goto continue end
local var = something
some code
::continue::
end
可能有人认为,这个 goto 语句跳转到了变量 var 的作用域内 。 但实际上这个 continue 标签出现在该代码块的最后一个有效语句后,因此 goto 并未跳转进入变量 var 的作用域内 。
goto 语句在编写状态机时也很有用 。 示例 8.1 给出了一个用于检验输入是否包含偶数个0的程序。
示例 8.1 一个使用 goto 语句的状态机的示例
::s1:: do
local c = io.read(1)
if c == '0' then goto s2
elseif c == nil then print ('ok'); return
else goto s1
end
end
::s2:: do
local c = io.read(1)
if c == '0' then goto s1
elseif c == nil then print('not ok'); return
else goto s2
end
end
goto s1
虽然可以使用更好的方式来编写这段代码,但上例中的方法有助于将一个有限自动机自动地转化为 Lua 语言代码。
再举一个简单的迷宫游戏的例子。 迷宫中有几个房间,每个房间的东南西北方向各有一扇门。 玩家每次可以输入移动的方向,如果在这个方向上有一扇门 , 则玩家可以进入相应的房间,否则程序输出一个警告,玩家的最终目的是从第一个房间走到最后一个房间 。
这个游戏是一个典型的状态机,当前玩家所在房间就是一个状态。 为实现这个迷宫游戏,我们可以为每个房间对应的逻辑编写一段代码,然后用 goto 语句表示从一个房间移动到另一个房间。示例 8.2展示了如何编写一个由 4 个房间组成的小迷宫。
示例 8.2 一个迷宫游戏
goto room1 -- 起始房间
::room1:: do
local move = io.read()
if move == "south" then goto room3
elseif move == "east" then goto room2
else
print("invalid move")
goto room1 -- 待在同一个房间
end
end
::room2:: do
local move = io.read()
if move == "south" then goto room4
elseif move == "west" then goto room1
else
print("invalid move")
goto room2 -- 待在同一个房间
end
end
::room3:: do
local move = io.read()
if move == "north" then goto room1
elseif move == "east" then goto room4
else
print("invalid move")
goto room3 -- 待在同一个房间
end
end
::room4:: do
print("Congratulations, you win!")
end
对于这个简单的游戏,使用数据驱动编程(使用表来描述房间和移动)是一种更好的设计方法。 不过,如果游戏中的每间房都各自不同,那么就非常适合使用这种状态机的实现方法。