Skip to content

第四章 语句

​ Lua同C或者Pascal等语言一样,支持常规语句,这些常规语句包括赋值、控制结构和过程调用;Lua也支持一些非常用语句,比如多重赋值和局部变量声明。

4.1 赋值

​ 赋值是更改一个变量值或表中键值的最基本手段:

lua
a = "hello" .. "world" t.n = t.n +

Lua允许多重赋值,即仅需一个步骤便将多个值分别赋给多个变量。变量和值之间通过逗号分隔,例如下面的语句:

lua
a, b = 10, 2*x

​ 变量a被赋值为10,变量b被赋值为2*x。

​ 在多重赋值中,Lua首先会计算出等号右边所有值得结果,然后再调用赋值。因此,可以利用多重赋值交换两个变量的值,比如下面的例子:

lua
x, y = y, x -- 交换'x'和'y'的值

a[i], a[j] = a[j], a[i] -- 交换'a[i]'和'a[j]'的值

Lua会自动调整值的数量来适应变量的数量:当值的数量比变量的数量少的时候,Lua会使用nil替代缺少的值;当值得数量比变量的数量多的时候,Lua会忽略多出来的值:

lua
a, b, c = 0, 1
print(a, b, c) --> 0 1 nil
a, b = a+1, b+1, b+2 -- b+2的值被忽略
print(a, b) --> 1 2 a, b, c = 0
print(a, b, c) --> 0 nil nil

上面例子中的最后一个赋值是一个常见错误:请牢记,想要在多重复值中初始化每个变量,必须逐个为它们提供值。

lua
a, b, c = 0, 0, 0
print(a, b, c) --> 0 0 0

​ 实际上,上面那些例子仅仅是为了说明多重赋值的使用方法,在现实中,人们很少将几个毫无关联的变量在同一行中使用多重赋值进行赋值。特别是,使用多重赋值并不比将赋值语句分开写在每一个独立行中效率更高。然后有些情况的确需要用到多重赋值,前面那个交换两个变量的值就是一个典型的例子。还有一种常见的情况是在一个函数有多个返回值时,单次调用并保存它的返回结果,这就要求在一个表达式中能够保存多个变量的值。例如赋值语句a,b=f()意味着函数f的调用返回两个结果:将第一个结果保存 结到a中,第二个结果保存到b中。

4.2 局部变量和块

​ Lua除了支持全局变量,同时也支持局部变量。使用local 关键字来创建局部变量:

lua
j = 10 -- 全局变量

local i = 1 -- 局部变量

​ 与全局变量不同的是,局部变量的作用于局限在声明他们的块中。"块"是可以是控制结构或者函数的执行体,也可以是程序块(变量被声明时所处的文件或字符串):

lua
x = 10
local i = 1 -- 程序块中的局部变量
while i <= x do
	local x = i*2 -- 执行体中的局部变量
	print(x) --> 2, 4, 6, 8, ...
	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-end 关键词对包起来的块还可以用于精确控制某些变量的作用域:

lua
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)

​ 尽量使用局部变量是一个良好的编程习惯。使用局部变量能够避免因为命名冲突而导致全局环境被破坏的情况。除此之外,局部变量比全局变量的访问速度要快。总而言之,局部变量会在离开它的作用域之后消失,并且允许垃圾回收器释放它的值。

​ Lua将声明局部变量作为语句来处理,这意味着,可以在任何地方定义一个局部变量。被声明变量的作用域从声明那一刻开始,到块末尾结束。变量声明的同时可以进行初始化赋值,遵循常规原则:多余的值会被丢弃;剩余未被赋值的变量被赋值为nil。如果一个声明没有初始化值,那么其中的变量都会被赋值为nil

lua
local a, b = 1, 10
if a < b then
	print(a) --> 1
	local a -- a被默认赋值为'nil'
	print(a) --> nil
end -- 以'then'开始的块结束
print(a, b) --> 1 10

Lua中的一个常见用法是:

lua
local foo = foo

​ 这行代码创建一个局部变量,并使用全局变量foo的值对其进行初始化(只有在声明之后,局部变量 foo才可见)。代码中的其它函数可能会对全局变量foo进行修改,而这个用法可以将foo的原始值进行保存。同时,访问局部变量的速度要快于访问全局变量,这也是这种用法的意义所在。

​ 因为许多编程语言要求在一个块(或者过程)的起始必须声明所有将要用到的局部变量,所以人们觉得在块的中间声明一个变量是不好的习惯。现实是,恰恰相反:在需要的时候再声明变量不仅可以减少无法用有效值对其进行初始化的情况(也减少忘记对其初始化的情况),还可以缩小变量的作用范围,而这可以提高代码的可读性。

4.3 控制结构

Lua提供一些简单并且常用的控制结构:

if用于条件执行;while ,repeat 和for 用于循环。所有的控制语句都有一个显式的终止标识:end 用于终止 if 、for 和while 结构;until 用于终止repeat 结构。

​ 控制结构中的条件表达式可以返回任何值。Lua把除了false 和nil 的所有值都认为是真(特别需要注意的是,数字0和空字符串也是真),这点需要牢记。

if then else

​ if语句会对它的条件进行测试,如果测试结果为真,就运行then部分 ,如果测试为假,在有else部 分的时候,运行else部分。else部分 不是必须的。

lua
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 ,它的用法跟if后面接else 类似,但可以免去使用多个end :

lua
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 语句,类似上面的用法跟switch 其实是大同小异的。while会循环检查条件是否为真,如果为真就会执行循环体中的代码,直到条件变为假。跟其它语言中的做法一样,Lua首先会对条件进行判断,如果为假,会直接结束循环;否则,Lua会执行循环体中的代码并重复这整个过程(判断和执行)。

lua
local i = 1
while a[i] do
	print(a[i])
	i = i + 1
end

repeat

​ repeat-until语句会重复执行循环体,直到条件为真。repeat-until语句会先执行循环体,再进行条件判断,因此,该语句可以保证至少执行一次循环体。

lua
--打印出第一行非空输入
repeat
line = io.read()
until line ~= ""
print(line)

不同于其它多数语言,Lua在循环体中声明的变量,作用范围延伸至条件判断部分:

lua
local sqr =  x/2
repeat
sqr = (sqr + x/sqr)/2
local error = math.abs(sqr^2 - x)
until error < x/10000 -- 局部变量'error'依旧可见

数字型for

​ for语句有两种形式:数字型for和泛型for。

​ 数字型for遵循下面的语法结构:

lua
for var = exp1, exp2, exp3 do
	<something>
end

上面的循环会将var变量初始设置为exp1,按照exp3的值步进并执行<something>中的代码内容,直到var的值大于exp2时结束。第三个表达式是可选的,如果没有,Lua会默认使用1作为步进值。下面是该循环使用的典型例子:

lua
for i = 1, f(x) do print(i) end

for i = 10, 1, -1 do print(i) end

如果想让循环没有上线,可以使用常熟math.huge:

lua
for i = 1, math.huge do
	if (0.3*i^3 - 20*i^2 - 500 >= 0) then
		print(i)
		break
	end
end

​ 为了合理使用for循环,有几点题需要注意:第一,在循环开始之前,exp1、exp2、exp3这三个表达式只会进行一次估值。比如,在第一个实例中,f(x)只会执行一次(之后的循环,f(x)将使用第一次计算的结果,而不会重新计算);第二,控制变量是for语句自动声明的局部变量,它只在循环体中可见。一个典型的错误是在循环结束后继续使用这个变量:

lua
for i = 1, 10 do print(i) end
max = i -- 这个i跟循环体中的i不一样,此处的i是全局变量

如果需要在循环结束之后继续使用控制变量(通常在中断循环之后),那就必须要把它保存到另一个变量中:

lua
-- 从表中查找变量
local found = nil
for i = 1, #a do
	if a[i] < 0 then
        found = i -- 保存'i'的值
        break
	end
end

print(found)

第三,一定不要手动修改控制变量的值:这要做的后果是不可预知的。如果需要非正常终止for循环,只需要使用break语句(就像前面的那个例子中那样)。

泛型for

​ 泛型for使用迭代函数来遍历所有值

lua
-- 打印表 't'的所有值
for k, v in pairs(t) do print(k, v) end

​ 上面的例子中使用了pairs来作为遍历表格的迭代函数,它是在Lua基本库中定义的。循环的每一步,k被赋值为一个键名,同时v被赋值为对应该键名的键值。

​ 除了这种简单的实例,泛型for还有更强大的用法。通过使用恰当的迭代器,几乎可以遍历任何以可读形式保存的数据。标准库中提供了多个迭代器,可以通过使用它们来遍历一个文件的所有行(io.lines)、表的键对(pairs)、序列的条目(ipairs)、字符串中的字符(string.gmatch)等等。

​ 当然,也可以实现自己的迭代器。虽然使用泛型for很容易,但设计对应的迭代函数却有很多玄机。因此,第七节将单独讨论这个话题。

​ 泛型循环跟数字型循环有两个共同之处:循环变量是只对循环体可见的局部变量;永远不要对它们进行赋值。

​ 看一个更加具体的使用泛型for的例子,假设有一个包含一周七天名称的表:

lua
days = {"Sunday", "Monday", "Tuesday", "Wednesday","Thursday", "Friday", "Saturday"}

​ 现在想根据某一天的名称得到它在这一周中的位置。虽然可以通过搜索获取到想要的结果,但以后大家就会知道,“搜索”在Lua中是极少使用的。一个更有效的解决方案是构造一个键名和键值对调的“逆转表”,比如叫revDays,它使用名字作为索引,使用数字作为值。这个表格看起来可能是下面的样子:

lua
revDays = {["Sunday"] = 1, ["Monday"] = 2,
["Tuesday"] = 3, ["Wednesday"] = 4,
["Thursday"] = 5, ["Friday"] = 6,
["Saturday"] = 7}

现在,想要查找某一天对应在一周中的位置,只需要对这个“逆转表”进行索引:

lua
x = "Tuesday"
print(revDays[x]) --> 3

当然,不需要对这个“逆转表”进行完全的初始化声明,而是可以利用既有的正常表,自动生成它:

lua
revDays = {}
for k,v in pairs(days) do
	revDays[v] = k
end

上面的循环通过变量k对应的键名(1,2,...)和变量v对应的键值(“Sunday”、“Monday”,...)将一周中的每一天都重新赋值。

4.4 break,return和goto

​ break和return语句允许从一个块中跳出。goto语句允许跳到函数中的几乎任意位置。

​ 可以使用break结束循环,这个语句可以中断包含它的最近的(比如多个循环嵌套)循环

(for,repeat和while);不可以在循环外使用break。循环中断后,程序从距离被中断的循环结构最近的位置继续执行。

​ return语句用于返回一个函数不常出现的结果,或者仅仅用于函数返回。任何函数的结尾都有一个隐含的return,因此,如果函数不需要返回任何值,那么如果它可以自然结束,就不需要再写一个return了。

​ 因为语法上的原因,如果要使用return,它就必须作为块中的最后一条语句。换句话说,return只能是程序块中的最后一条语句,或者紧接end、else或者until。比如下面的例子中,return就是then块中的

最后一条语句:

lua
local i = 1
while a[i] do
    if a[i] == v then return i end
    i = i + 1
end

通常情况下,因为return后面的语句都不可达,上面提到可以用return的地方是满足需求的。但有时候,需要在一个块的中间插入return避免后面代码的运行来进行调试,这种情况下,可以使用do-end块把return语句包含起来:

lua
function foo ()
    return --<< 语法错误
    <other statements>
end

function foo ()
    do return end -- 语法正确,因为return是do-end块中的最后一条语句
    <other statements>
end

​ goto语句可以跳出程序执行到指定标签中。关于goto的使用经历了漫长的讨论,知道现在都有人坚持认为goto语句对于编程有害,应当从编程语言中被移除。尽管如此,流行的几种语言都提供了goto语法,因为它们都能找到使用goto的很好的理由。goto是一个强大的机制,小心并合理使用不仅无害,而且可以大大提高代码的质量。

​ Lua中goto语句的语法十分常规:预留字goto后面紧跟符合标识符标准的任意标签名。但定义标签的语法有点让人费解:使用两对冒号(::)将标签名包起来,就像::name::这样。其实这种啰嗦的做法是故意的,目的是让程序员在使用goto之前能够三思。

​ Lua对允许使用goto的地方做了一些限制。首先,便签遵从通常定义的可视化规则,因此无法跳入一个块(因为块中的标签在块外是不可见的);其次,无法跳出函数(注意,第一条限制已经杜绝了跳入函数的可能性);最后,goto不可以跳入局部变量的作用域。

​ 一个典型并且合理使用goto的情景是利用goto模拟其它语言中具备而在Lua中被抛弃的结构用法,比如continue、多级break、多级continue、redo、局部异常处理,等等。continue语句其实仅仅就是goto跳入块尾定义的标签;redo语句则是跳到块起始定义的标签:

lua
while 某种_条件 do
    ::redo::
    if 分支条件_1 then goto continue
    else if 分支条件_2 then goto redo
    end
    <其它代码>
    ::continue::
end

​ 需要注意的是,Lua的规则中对于局部变量作用域的定义有一个有意思的细节:局部变量的作用域从块中定义这个变量的位置开始,直到块最后一个“非空语句”止。而标签恰恰属于“空语句”。为了理解这个细节的意思,可以看一下下面两个例子(译者注:第二个例子及内容是译者个人添加):

lua
while 某种_条件 do
    if 分支条件 then goto continue end -----此处的goto正常
    local var = something
    <其它代码> -----var作用域结束的地方
    ::continue::
end


while 某种_条件 do
    if 分支条件 then goto continue end -----此处的goto有语法错误
    local var = something
    <其它代码>
    ::continue::
    print(var) -----var作用域结束的地方
end

​ 在第一个例子中,可能大家会认为goto语句跳入了局部变量var的作用域。其实,continue标签作为“空语句”,位于块中所有“非空语句”之后,此时,它已经不在局部变量var的作用域中,goto自然没有问题;而对于第二种情况,continue标签之后还有“非空语句”print,这导致print成为了块中最后一条“非空语句”并成为var作用域的结束之处,这导致continue标签被包含在了局部变量var的作用域中,导致goto语句出现语法错误。

goto语句在编写状态机的时候十分有用。下面的代码展示如何用0作为状态切换的标志在两个块中进行切换。当然,在处理具体的问题时可以进行更好的优化,但这是在Lua中引入有限自动机进行自动编码的极佳思路(考虑一下动态代码生成)。

lua
::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

​ 再举一个例子,假设要做一个简单的迷宫游戏:迷宫有几个房间,每个房间最多有四个门——东、南、西、北。每一步,玩家输入一个移动方向,如果这个方向有门,玩家可以进入对应的下一个房间;否则,打印警告。游戏的目标就是从最初的房间进入最后的房间。

​ 这个游戏是一个典型的状态机,当前的房间就是状态。可以把迷宫的每个房间都实现成一个程序块,使用goto在房间之间切换。下面的代码就演示了如何写一个有四个房间的小迷宫。

lua
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 -- stay in the same room
    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 won!")
end
function getlabel ()
    return function () goto L1 end
    ::L1::
    return 0
end

​ 对于这个这个简单的例子,一个更好的设计是基于数据驱动编程,即使用表来表示房间和移动。但无论怎样,游戏中的每个房间都有几个特定的状态,因此“状态机”设计是十分符合需求的。

Released under the MIT License.