3.3 -- 语句

Lua 支持所有与 Pascal 或是 C 类似的常见形式的语句, 这个集合包括赋值,控制结构,函数调用,还有变量声明。

3.3.1 – 语句块

语句块是一个语句序列,它们会按次序执行:

block ::= {stat}

Lua 支持 空语句, 你可以用分号分割语句,也可以以分号开始一个语句块, 或是连着写两个分号:

stat ::= ‘;’

函数调用和赋值语句都可能以一个小括号打头, 这可能让 Lua 的语法产生歧义。 我们来看看下面的代码片断:

a = b + c
     (print or io.write)('done')

从语法上说,可能有两种解释方式:

a = b + c(print or io.write)('done')
     
     a = b + c; (print or io.write)('done')

当前的解析器总是用第一种结构来解析, 它会将开括号看成函数调用的参数传递开始处。 为了避免这种二义性, 在一条语句以小括号开头时,前面放一个分号是个好习惯:

;(print or io.write)('done')

一个语句块可以被显式的定界为单条语句:

stat ::= do block end

显式的对一个块定界通常用来控制内部变量声明的作用域。 有时,显式定界也用于在一个语句块中间插入 return (参见 §3.3.4)。

3.3.2 – 代码块

Lua 的一个编译单元被称为一个 代码块。 从句法构成上讲,一个代码块就是一个语句块。

chunk ::= block

Lua 把一个代码块当作一个拥有不定参数的匿名函数 (参见§3.4.11)来处理。 正是这样,代码块内可以定义局部变量,它可以接收参数,返回若干值。 此外,这个匿名函数在编译时还为它的作用域绑定了一个外部局部变量 _ENV (参见 §2.2)。 该函数总是把 _ENV 作为它唯一的一个上值, 即使这个函数不使用这个变量,它也存在。

代码块可以被保存在文件中,也可以作为宿主程序内部的一个字符串。 要执行一个代码块, 首先要让 Lua 加载 它, 将代码块中的代码预编译成虚拟机中的指令, 而后,Lua 用虚拟机解释器来运行编译后的代码。

代码块可以被预编译为二进制形式; 参见程序 luac 以及函数 string.dump 可获得更多细节。 用源码表示的程序和编译后的形式可自由替换; Lua 会自动检测文件格式做相应的处理 (参见 load)。

3.3.3 – 赋值

Lua 允许多重赋值。 因此,赋值的语法定义是等号左边放一个变量列表, 而等号右边放一个表达式列表。 两边的列表中的元素都用逗号间开:

stat ::= varlist ‘=’ explist
	varlist ::= var {, var}
	explist ::= exp {‘,’ exp}

表达式放在 §3.4 中讨论。

在作赋值操作之前, 那值列表会被 调整 为左边变量列表的个数。 如果值比需要的更多的话,多余的值就被扔掉。 如果值的数量不够需求, 将会按所需扩展若干个 nil 。 如果表达式列表以一个函数调用结束, 这个函数所返回的所有值都会在调整操作之前被置入值列表中 (除非这个函数调用被用括号括了起来;参见 §3.4)。

赋值语句首先让所有的表达式完成运算, 之后再做赋值操作。 因此,下面这段代码

i = 3
     i, a[i] = i+1, 20

会把 a[3] 设置为 20,而不会影响到 a[4] 。 这是因为 a[i] 中的 i 在被赋值为 4 之前就被计算出来了(当时是 3 )。 简单说 ,这样一行

x, y = y, x

会交换 xy 的值, 及

x, y, z = y, z, x

会轮换 xyz 的值。

对全局变量以及表的域的赋值操作的含义可以通过元表来改变。 对 t[i] = val 这样的变量索引赋值, 等价于 settable_event(t,i,val)。 (关于函数 settable_event 的详细说明,参见 §2.4。 这个函数并没有在 Lua 中定义出来,也不可以被调用。 这里我们列出来,仅仅出于方便解释的目的。)

对于全局变量 x = val 的赋值等价于 _ENV.x = val (参见 §2.2)。

3.3.4 – 控制结构

if , while , and repeat 这些控制结构符合通常的意义,而且也有类似的语法:

stat ::= while exp do block end
	stat ::= repeat block until exp
	stat ::= if exp then block {elseif exp then block} [else block] end

Lua 也有一个 for 语句,它有两种形式 (参见 §3.3.5)。

控制结构中的条件表达式可以返回任何值。 falsenil 两者都被认为是假。 所有不同于 nilfalse 的其它值都被认为是真 (特别需要注意的是,数字 0 和空字符串也被认为是真)。

repeatuntil 循环中, 内部语句块的结束点不是在 until 这个关键字处, 它还包括了其后的条件表达式。 因此,条件表达式中可以使用循环内部语句块中的定义的局部变量。

goto 语句将程序的控制点转移到一个标签处。 由于句法上的原因, Lua 里的标签也被认为是语句:

stat ::= goto Name
	stat ::= label
	label ::= :: Name ::

除了在内嵌函数中,以及在内嵌语句块中定义了同名标签,的情况外, 标签对于它定义所在的整个语句块可见。 只要 goto 没有进入一个新的局部变量的作用域,它可以跳转到任意可见标签处。

标签和没有内容的语句被称为空语句,它们不做任何操作。

break 被用来结束 whilerepeat 、或 for 循环, 它将跳到循环外接着之后的语句运行:

stat ::= break

break 跳出最内层的循环。

return 被用于从函数或是代码块(其实它就是一个函数) 中返回值。 函数可以返回不止一个值,所以 return 的语法为

stat ::= return [explist] [‘;’]

return 只能被写在一个语句块的最后一句。 如果你真的需要从语句块的中间 return , 你可以使用显式的定义一个内部语句块, 一般写作 do return end。 可以这样写是因为现在 return 成了(内部)语句块的最后一句了。

3.3.5 – For 语句

for 有两种形式:一种是数字形式,另一种是通用形式。

数字形式的 for 循环,通过一个数学运算不断地运行内部的代码块。 下面是它的语法:

stat ::= for Name ‘=’ exp ‘,’ exp [‘,’ exp] do block end

block 将把 name 作循环变量。 从第一个 exp 开始起,直到第二个 exp 的值为止, 其步长为第三个 exp 。 更确切的说,一个 for 循环看起来是这个样子

for v = e1, e2, e3 do block end

这等价于代码:

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

注意下面这几点:

  • 所有三个控制表达式都只被运算一次, 表达式的计算在循环开始之前。 这些表达式的结果必须是数字。
  • varlimit,以及 step 都是一些不可见的变量。 这里给它们起的名字都仅仅用于解释方便。
  • 如果第三个表达式(步长)没有给出,会把步长设为 1 。
  • 你可以用 breakgoto 来退出 for 循环。
  • 循环变量 v 是一个循环内部的局部变量; 如果你需要在循环结束后使用这个值, 在退出循环前把它赋给另一个变量。

通用形式的 for 通过一个叫作 迭代器 的函数工作。 每次迭代,迭代器函数都会被调用以产生一个新的值, 当这个值为 nil 时,循环停止。 通用形式的 for 循环的语法如下:

stat ::= for namelist in explist do block end
	namelist ::= Name {, Name}

这样的 for 语句

for var_1, ···, var_n in explist do block end

它等价于这样一段代码:

do
       local f, s, var = explist
       while true do
         local var_1, ···, var_n = f(s, var)
         if var_1 == nil then break end
         var = var_1
         block
       end
     end

注意以下几点:

  • explist 只会被计算一次。 它返回三个值, 一个 迭代器 函数, 一个 状态 , 一个 迭代器的初始值
  • fs,与 var 都是不可见的变量。 这里给它们起的名字都只是为了解说方便。
  • 你可以使用 break 来跳出 for 循环。
  • 环变量 var_i 对于循环来说是一个局部变量; 你不可以在 for 循环结束后继续使用。 如果你需要保留这些值,那么就在循环跳出或结束前赋值到别的变量里去。

3.3.6 – 函数调用语句

为了允许使用函数的副作用, 函数调用可以被作为一个语句执行:

stat ::= functioncall

在这种情况下,所有的返回值都被舍弃。 函数调用在 §3.4.10 中解释。

3.3.7 – 局部声明

局部变量可以在语句块中任何地方声明。 声明可以包含一个初始化赋值操作:

stat ::= local namelist [‘=’ explist]

如果有初始化值的话,初始化赋值操作的语法和赋值操作一致 (参见 §3.3.3 )。 若没有初始化值,所有的变量都被初始化为 nil

一个代码块同时也是一个语句块(参见 §3.3.2), 所以局部变量可以放在代码块中那些显式注明的语句块之外。

局部变量的可见性规则在 §3.5 中解释。