本章节主要是讲解Lua的词法,语法跟语义。通俗点来说就是描述哪些词是有效的,如何组合跟组合后是什么意思。
语言构造将使用常用的扩展BFN表达式写出,也就是这样:{a}表示0或n个a,[a]表示一个选项a。非最终符号可以写成non-terminal,关键字会写成kword,而不能分解的符合会写成这样‘=’。完整的Lua语法可以查看最后一章(见:8)。
2 – 语言定义
2.1 词法约定
Lua语言格式是很自由的。它会忽略语法元素(符号)之间的空格(换行符)和注释,尽把它们当做名字与关键字的分隔符。
Lua中的名字(也叫标识符)可以是由非数字开头的任意字符数字和下划线构成的非预定字符串。标识符可以用于变量,表字段和标签命名。
以下是保留关键字不能用于命名:
and break do else elseif end
false for function goto if in
local nil not or repeat return
then true until while
Lua是区分大小写的语言;and是个保留关键字,而And跟AND都两个不同的有效名字。作为一个约定,程序应避免创建以下划线加一个或多个大写字母构成的名字(例如_VERSION)。
以下是一些其他符号:
+ - * / % ^ #
& ~ | << >> //
== ~= <= >= < > =
( ) { } [ ] ::
; : , . .. ...
字符串可以用单引号或双引号括起来,也可以包括C风格的转义符;’\a’(响铃),’\b’(退格符),’\f’(换页符),’\n’(换行符),’\r’(回车符),’\t’(横向制表符),’\v’(纵向制表),’\\’(反斜杠),’\”‘(双引号)以及’\”(单引号)。在反斜杠后面加个真正的换行就等于在字符串里写个换行符。转义符’\z’会忽略后面的一系列空白符跟换行符;它在需要对一个很长的字符串进行断行并希望在每个新行保持缩进而又不真正需要这些符号时非常有用。
Lua的字符串是可以保存8位的任意值,其中包括用’\0’表示0。一般而言,你可以用字符的数字值来表示这个字符。用转义符\xXX方式,这里的XX必须是两个16进制字符,或者用\ddd转义符,而ddd则是必须是1到3个十进制字符。(注意,如果转义符接着后面还是数字的话,那么这个转义符必须写满3位。)
对于用UTF-8编码Unicode字符可以用转义符\u{XXX}(必须要一对花括号),这里的XXX是16进制的字符编码。
可以使用方括号括起来来定义一个字符串。我们在两个方括号之间插入n个等号来表示第n级开的方括号。因此一个0级开的方括号写作[[,1级开的方括号写作[=[,以此类推。闭方括号也作类似的定义;举个例子,一个4级闭的方括号写作]====]。一个长字符串有任何级的开方括号开始并由同级的闭方括号结束。这样的描述可以包含除了同一级别闭方括号外的任意字符。在这个形式下是不受分行限制的,不解析任何转义符,并且忽略任何级的方括号。其中任何形式的换行串(回车,换行,回车加换行,换行加回车)都会被转换成单个换行符。
字符串中的每个不被上述规则所影响的字符都呈现本身。然而Lua采用文本模式打开文件,一些系统文件操作函数对某些控制符处理可能存在问题。因此对于非文本数据采用括号括起来并显式的用转义符规则来表述更为安全。
为了方便起见,当左方括号紧跟一个换行符时,这个换行符是无效的。举个例子,在使用ASCII的系统中(其中’a’的编码为97,换行符的编码为10和’1’的编码为49)。下面5个方式都表示同一个字符串:
a = 'alo\n123"' a = "alo\n123\"" a = '\97lo\10\04923"' a = [[alo 123"]] a = [==[ alo 123"]==]
数字常量(或称数字量)可以又可选的小数部分和可选的10为底的指数构成,指数用’e’或’E’来标记。Lua可以接受由0x或0X开头的十六进制常量。十六进制常量也可以又可选小数部分加可选的2为底的指数构成,指数用’p’或’P’来标记。带有小数点或指数的数字常量都被认为是浮点数;否则被认为是整数。下面是合法的整数常量:
3 345 0xff 0xBEBADA
下面是合法的浮点常量:
3.0 3.1416 314.16e-2 0.31416E1 34e1 0x0.1E 0xA23p-4 0X1.921FB54442D18P+1
在字符串以外地方出现双连接符(–)开头都表示注释。如果–后面没有紧跟着一个左方括号,注释为短注释,他都注释内容到这行的行末为止。否则它就是一个长注释,它是以右方括号结束,通常用来临时屏蔽代码。
2.2 变量
变量是存储值的地方。在Lua中有三种类型变量:全局变量,局部变量和表字段。
单个名字可以是全局变量也可以是局部变量(或者是函数的参数,这是一种特别的局部变量):
var ::= Name
名字是指3.1所定义的标识符。
任何没有明确声明为局部变量都被当做全局变量(见:2.3.7)。局部变量有其作用范围:局部变量可以被定义在作用范围中的函数任意使用(见:2.5)。
在第一次对变量进行赋值前,它的值为nil。
方括号被用做表的索引:
var ::= prefixexp ‘[’ exp ‘]’
可以同过元表来改变全局变量做为表字段进行访问。以索引方式访问一个变量t[i]等价于调用gettable_event(t,i)。(见:1.4完整的关于gettable_event这个函数的说明。在Lua中它并没有定义也不能调用。这里提到它只是为了更好的解释问题。)
var.Name 只是 var[“Name”] 的变种而已:
var ::= prefixexp ‘.’ Name
访问一个全局变量x就等同于访问_ENV.x。由于采用代码块编译方式,_ENV从来都不是一个全局变量(见:1.2)。
2.3 语句
Lua支持所有与 Pascal 或者 C 类似的常见形式的语句,包括了赋值,结构控制,函数调用和变量声明。
2.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(见:2.3.4)。
2.3.2 代码块
在Lua中一个可编译单元就叫代码块。在语法构造上来说,代码块就是简单的语句块:
chunk ::= block
Lua把代码块当做一个不定参数的匿名函数(见:2.4.11)来处理。也就是说代码块可以定义局部变量,接受参数跟返回结果。此外这样的匿名函数在编译时在作用范围内还会扩展一个外部局部变量_ENV(见:1.2)。因此这个函数总会把_ENV做为唯一的上值,尽管都没有使用到它。
代码块可以是一个文件或在寄主程序内部的一段字符串。运行代码块,Lua首先会加载它,预编译成虚拟机可以运行的指令,然后Lua用虚拟机解析器来运行这些编译后的代码。
代码块可以被预编译成二进制形式;参考luac程序和string.dump函数获取详细信息。源码程序和编译后的形式是可以替换的;Lua会根据类型做出相对应的处理(见:load)。
2.3.3 赋值
Lua允许多重赋值。因此赋值语法定义是在等号的左边为变量列表,右边为表达式列表。列表中的元素之间用逗号隔开:
stat ::= varlist ‘=’ explist
varlist ::= var {‘,’ var}
explist ::= exp {‘,’ exp}
表达式放在§2.4讨论。
在赋值操作之前,值列表会被调整到跟变量列表个数一样。如果值的个数比较多,那么多出来的将被抛弃掉。如果值个数比较少,那么将用 nil 来扩展值列表到一样为止。如果表达式列表是以一个函数调用结尾,那么这个函数的所有返回值在调整值列表之前置入列表中(除非这个函数调用被括号括起来;见:2.4)。
赋值语句是先执行完所有的表达式后再进行赋值操作。因此下面代码
i = 3 i, a[i] = i+1, 20
会把 a[3] 设置为20,而不影响到 a[4],这是因为 a[i] 的i在被赋值为4之前就已经计算出来了(值为3)。简单说这样一行
x, y = y, x
可以交换x,y的值,以及
x, y, z = y, z, x
可以轮流交换x,y和z的值。
对全局变量以及表字段的赋值操作的含义可以通过元表来改变。以索引方式赋值一个变量 t[i] = val 等价于调用settable_event(t,i,val)。(见:1.4完整的关于settable_event这个函数的说明。在Lua中它并没有定义也不能调用。这里提到它只是为了更好的解释问题。)
对全局变量 x = val 赋值就等同于_ENV.x = val(见:1.2)。
2.3.4 控制结构
if,while 和 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 语句,它有两种形式(见:2.3.5)。
控制结构中的条件表达式可以返回任意值。只有 false 和 nil 被认为是非真的。所有不是 false 和 nil 的其它值都被认为真(特别提下,0和空字符串也是为真)。
在 repeat–until 这个循环中,内部语句块的结束点不在 until 处,而是在它后面的条件语句。因此条件语句中可以引用在内部语句块中声明的变量。
goto 可以将程序的控制点转移到一个标签处。由于语法上的原因,Lua里的标签也是被认为语句:
stat ::= goto Name
stat ::= label
label ::= ‘::’ Name ‘::’
除了内嵌函数和内嵌语句块中的同名标签外,标签对于它定义所在的整个语句块是可见的。只有 goto 不进入一个新局部变量的控制范围内,它都可以跳转到任意可以见标签处。
标签和没有内容的语句被叫做空语句,因为它是进行任何动作。
break 被用来终止 while,repeat 和 for 循环,它跳转到循环外接着执行后面的语句:
stat ::= break
break 跳出最内层的循环。
return 常被用于从函数或代码块(匿名函数)中的返回结果集。函数可以返回一个或多个值,所以 return 的语法如下所示:
stat ::= return [explist] [‘;’]
return 只能出现一个语句块的最后一行。如果真的需要在语句块中间 return,你可以显示的定义一个内部语句块,一般写成do return end,这样写就说明 return 在(内部)语句块的最后一句了。
2.3.5 For 语句
for 有两种形式:一种是数字形式,另一种是通用形式。
数字形式的 for 是通过一个算术运算不断运行内部的代码块。它的语法如下:
stat ::= for Name ‘=’ exp ‘,’ exp [‘,’ exp] do block end
语句块是从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
注意以下几点:
- 这个三个控制表达式在循环之前就被运行一次。而且其结果必须是数字。
- var,limit 和 step 都是不可见的变量,在这里只是为了更好的解释说明而已。
- 如果不存在第三个变量(step),那么就会把它设为1。
- 可以使用 break 或 goto 退出一个 for 循环。
- 循环变量 v 是循环体的内部变量。如果在循环外部还要用到它的值,可以在退出循环前将它赋值给另一个变量。
通用形式的 for 是通过一个迭代器的函数进行工作。每次迭代,迭代器函数都被调用并返回一个新的值,当返回值为 nil 时就停止循环。它的语法如下:
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 只会被运算一次。并返回三个值,分别是迭代函数,状态和迭代器的初始值。
- f,s和var都不是可见的变量,在这里只是为了更好的解释说明而已。
- 可以用 break 来退出 for 循环。
- 循环变量 var_i 是循环的局部变量;如果在循环外部还要用到它的值,可以在跳出或退出循环前将它赋值给另一个变量。
2.3.6 函数调用语句
为了允许函数的副作用,函数调用可以被当作一个语句执行:
stat ::= functioncall
在这种情况下,函数所有的返回值都会被丢弃。在§2.4.10将会对函数调用进行详细说明。
2.3.7 局部声明
在语句内部的任何地方都可以声明局部变量。声明可以包含一个初始化赋值操作:
stat ::= local namelist [‘=’ explist]
如果存在初始化赋值的话,那么初始化赋值具备与多重赋值相同的语义(见:2.3.3)。否则所有的变量都会被初始化为 nil。
一个代码块就相当于语句块(见:2.3.2),所以局部变量可以在那些显式外部语句块外的代码块内声明。
局部变量的可见规则在§2.5阐述。
2.4 表达式
以下就是Lua的基本表达式:
exp ::= prefixexp
exp ::= nil | false | true
exp ::= Numeral
exp ::= LiteralString
exp ::= functiondef
exp ::= tableconstructor
exp ::= ‘…’
exp ::= exp binop exp
exp ::= unop exp
prefixexp ::= var | functioncall | ‘(’ exp ‘)’
数字和字符串在§2.1中阐述;变量在§2.2中阐述;函数声明在§2.4.11中阐述;函数调用在§2.4.10中阐述;表构建在§2.4.9中阐述;可变参数表达式写作三个点(…),它只能在有可变参数的函数中直接使用;它们在§2.4.11中阐述。
二进制运算符包括算数运算符(见:§2.4.1),位运算符(见:§2.4.1),比较运算符(见:§2.4.4),逻辑运算符(见:§2.4.5)跟连接运算符(见:§2.4.6).一元运算符包括负号(见:§2.4.1),按位非(见:§2.4.2),逻辑非(见:§2.4.5)和求长(见:§2.4.7)。
函数调用和可变参数表达式都可以返回多值结果。如果把函数调用当成一条语句(见:§2.3.6),那么将把它的返回结果调整为0个元素,也就是将抛弃所有结果。如果表达式为表达式列表的最后(唯一)一个元素,那么将不会做任何调整(除非用括号括起来)。其他情况下,Lua会把结果集调整为一个元素置入表达式中,保留第一个值而忽略其他值,当没有值则用 nil 填充上去。
下面有些例子:
f() -- 将结果调整为0个 g(f(), x) -- 将 f() 返回值调整为1个 g(x, f()) -- 函数 g 获得 x 加 f() 函数的结果集作为参数 a,b,c = f(), x -- 将f()返回结果调整为一个(c被赋值为 nil ) a,b = ... -- 变量 a 获得可变参数的第一个,而变量 b 则获得 -- 第二个 (如果可变参数没有实际参数时, -- a 和 b 将会被赋值为 nil) a,b,c = x, f() -- 函数 f() 的结果集被调整为2个元素 a,b,c = f() -- 函数 f() 的结果集被调整为3个元素 return f() -- 返回所有的函数 f() 的结果 return ... -- 返回所有接收到的可变参数 return x,y,f() -- 返回 x,y 和函数 f() 的结果集 {f()} -- 创建一个带有函数 f() 所有结果的列表 {...} -- 创建一个带有所有可变参数的列表 {f(), nil} -- 将函数 f() 返回值调整为1个
任何被括号括起来的表达式永远被当做一个值。也就是 ( f(x, y, z) )无论函数 f 返回多少个值,这个表达式都表示一个单值(这个表达式的值为函数 f 返回结果的第一个值,如果函数 f 没有返回值,则为 nil )。
2.4.1 算术运算操作
Lua支持以下算术运算操作:
- +:加法
- –:减法
- *:乘法
- /:除法
- //:向下取整除法
- %:取模
- ^:乘方
- –:取负
除了乘方和浮点除法外,其他的操作如下工作:如果两个操作数都是整型,那么按整型方式操作结果也是整型。否则当两个操作数是数字或可以转换成数字的字符串(见:§2.4.3),会将他们转换成浮点数,操作按照常规浮点规则(一般遵循 IEEE 754标准)而且结果为浮点型。
乘方和浮点除法(/)总是将操作数转换为浮点型且结果也为浮点型。乘方采用的是ISO C语言的 pow函数,因此它可以接受非整数指数。
向下取整除法(//)指做一次除法,并对商数被圆整到靠进负无穷的一侧,即对操作数进行除法后下取整。
取模就是取除法的余数,其商被圆整到靠进负无穷的一侧(向下取整除法)。
对于整数算术运算溢出的情况,这些操作通常都会遵循以 2 为补码的数学运算 环绕 规则。(也就是说它们的返回结果就是对 264 取模的结果。)
2.4.2 位运算操作
Lua支持以下位运算:
- &:按位与
- |:按位或
- ~:按位异或
- >>:右移
- <<:左移
- ~:一元按位非
所有的位运算都会把操作数转换为整型(见:§2.4.3),然后按位操作,结果返回也是整型。
左移跟右移都会用 0 来填充空白位。移动的位数为负数,则向反方向移动;若移动的位数的绝对值大于等于整数本身的位数,则结果为0(也就是所有的位被移掉)。
2.4.3 强制转换
Lua会对一些类型和值的内部表示在运行时做一些数字转换。位操作总是会把浮点型操作数转换成整型。乘方和浮点除法总是会把整型操作数转成浮点型。其它的数学操作若针对混合数字操作数(整型和浮点型)将整型转为浮点型;这是一个通用规则。C API会按需要把整型转为浮点型以及把浮点型转为整型。字符串连接除了字符串外也可以接受数字作为参数。
当操作需要数字时,Lua可以把字符串转换成数字。
当把一个整数转成浮点时,若整数值恰好可以表示为一个浮点时,那就取那个浮点。否则转换会取最接近的最大值或最小值来表示这个数。这种转换是不会失败。
把一个浮点型转成整型过程会检查浮点型恰好可以表示为一个整型(也就是浮点是一个整数值或在整数可以表达的范围内)。如果可以则就是取这个值,否则转换失败。
字符串转成数字过程遵循下面流程:首先遵循Lua词法分析器的规则和分析语法将字符串转换成对应的整数或浮点数。(字符串可以有前置或后置空格或一个符号。)然后结果数值(整数或浮点数)会根据语境转换成所需要的类型(例如:强制转换操作)。
将数字转换字符串使用非特定可读性的格式。若想完全控制数字转换字符串格式,可以使用字符串库类的format函数(见:string.format)。
2.4.4 比较操作
Lua支持下面的比较操作:
- ==: 等于
- ~=:不等于
- <:小于
- >:大于
- <=:小于等于
- >=:大于等于
这些操作的结果都是返回 true 或 false。
等于比较(==)首先对比下操作数的类型。如果类型不一样,结果就为 false。否则才对比操作数的值。字符串按一般的方式进行比较。若数字的数学值相同时则数字是相等的。
表,userdata和thread是通过引用进行比较:只有两者引用同一个对象时才认为这两个对象相等。每次新创建一个对象(表,userdata或thread),新对象一定与已存在的对象不同。相同引用的闭包一定相等。有任何可察觉差异(不同的行为,不同的定义)的闭包一定不相等。
可以采用 “eq” 元方法去改变Lua对表和userdata的比较方式。
等于操作不会将字符串转换为数字,反之亦然。即 “0” == 0 的结果为 false,而 t[0] 和 t[“0”] 表示表内的不同项。
~= 的操作完全等价于(==)相反操作。
比较操作按照下面的方式进行。如果参数都是数字,就会根据它们的数学值(不管它们的亚型)进行比较。否则若都是字符串,就会根据当前的语言环境进行比较。再不然Lua会试着调用 “lt” 或 “le” 元方法(见:§2.4)。a > b 的比较被译为 b < a,a >= b 被译为 b <= a。
2.4.5 逻辑操作
Lua有 and ,or 和 not 逻辑操作符。类似控制结构(见:§2.3.4),所有逻辑操作符把 false 和 nil 当做假,其它一切都当做真。
取反操作 not 的总是返回 true 或 false 其中一个。与操作符 and 在第一参数为 false 或 nil 就直接返回它,否则就返回第二个参数。或操作符 or 在第一参数不为 false 和 nil 就直接返回它,否则就返回第二个参数。and 或 or 都遵循短路规则;也就是说第二个参数只有在需要的时候才去求值。这里有一些例子:
10 or 20 –> 10
10 or error() –> 10
nil or “a” –> “a”
nil and 10 –> nil
false and error() –> false
false and nil –> false
false or nil –> nil
10 and 20 –> 20
(在本手册中,–> 指表达式的运行结果)。
2.4.6 字符串连接
Lua用两个点(..)定义字符串连接操作符。如果两个操作数都是字符串或数字,就按 §2.4.3 中提到规则进行转换成字符串。否则就调用__concat 元方法(见:§1.4)。
2.4.7 求长度运算
求长操作符采用一元前置符 # 来表示。字符串的长度是它的字节个数(就是以一个字符为一个字节来计算字符串长度)。
程序可以修改 __len 元方法来更改除了字符串类型外任何值的求长操作行为(见:§1.4)。
如果没有定义 __len 元方法,表 t 的长度只有在表为序列是才有定义,也就是序列是指正数键集等同于n为非负整数 {1…n} 。在这种情况下,n就是表的长度。注意下这样的表:
{10, 20, nil, 40}
它不是一个序列,因为它有键4却没有键3。(因为该表的正整数键集不等于 {1..n} 集合,故而就不存在n。)注意:表是否为一个序列跟它的非数字键无关。
2.4.8 优先级
Lua中的操作符优先级按下面的表从底到高排序:
or
and
< > <= >= ~= ==
|
~
&
<< >>
..
+ –
* / // %
一元操作 (not # – ~)
^
通常可以使用括号来更改表达式的优先级。连接操作符(..)跟乘方操作符(’^’)都是从右到左的。其他的二元操作符都是从左到右的。
2.4.9 表构建
表构建是一个构建表的表达式。每次构建句被执行,就创建一个新表。一个构建句可以创建一个新空表或一个表并初始化一些字段。构建句的一个语法如下所示:
tableconstructor ::= ‘{’ [fieldlist] ‘}’
fieldlist ::= field {fieldsep field} [fieldsep]
field ::= ‘[’ exp ‘]’ ‘=’ exp | Name ‘=’ exp | exp
fieldsep ::= ‘,’ | ‘;’
每个形式如 [exp1] = exp2 的域表示向表中添加一个新项,其键为 exp1 值为 exp2。形式 name = exp 的域等价于 [“name”] = exp。最后形式 exp 的域等价于 [i] = exp,这里的 i 是一个从 1 开始增长的整数。这个格式的域不会影响到计数。举个例子,
a = { [f(1)] = g; "x", "y"; x = 1, f(x), [30] = 23; 45 }
等价于
do local t = {} t[f(1)] = g t[1] = "x" -- 1st exp t[2] = "y" -- 2nd exp t.x = 1 -- t["x"] = 1 t[3] = f(x) -- 3rd exp t[30] = 23 t[4] = 45 -- 4th exp a = t end
构造句子中的赋值次序是没有被定义的。(次序问题只会对重复键的情况下有影响。)
如果列表中最后一个域的形式是 exp 并且表达式是一个函数调用或可变参数,那么这个表达式返回的所有值会依次放入列表中(见:§2.4.10)。
域列表的最后可以有一个分隔符,这样设计方便机器生产代码。
2.4.10 函数调用
Lua中函数调用的语法如下:
functioncall ::= prefixexp args
在函数调用中,首先 prefixexp 和 args 是先求值的。如果 prefixexp 的值类型是 function ,那这个函数被给出的参数调用。否则 prefixexp 的 “call” 元方法被调用,把 prefixexp 的值当做第一个参数,接下来就是原调用参数(见:§1.4)。
这样的形式
functioncall ::= prefixexp ‘:’ Name args
可以用来调用”方法”。v:name(args) 可以被解析为 v.name(v,args),这里的 v 只会被计算一次。
参数的语法如下:
args ::= ‘(’ [explist] ‘)’
args ::= tableconstructor
args ::= LiteralString
所有的参数表达式求值都在函数调用之前进行。这样调用形式 f{fields} 可以被解析为 f({fields}) ;这里的参数列表是新创建的一个单表。f ‘string‘(或者 f”string” 或者 f[[string]])这样的调用形式可以被解析为 f(‘string‘);这里的参数列表就是一个单独的字符串。
return functioncall 这样的调用形式会触发一次尾调用。Lua实现了完全尾调用(或称为完全尾递归):在尾调用中,被调用的函数会重新使用调用函数的堆栈项。因此对于程序执行嵌套尾调用的层数是没有限制的。然而一个尾调用可以清除调用它的函数的任何调试信息。注意尾调用只发生在特定的语法中,仅当 return 只有一个单独的函数调用作为参数时才发生尾调用;这种语法使得调用函数的所有结果被完整的返回。因此下面的例子都不是尾调用:
return (f(x)) -- 将结果调整为 1 个元素 return 2 * f(x) return x, f(x) -- 追加若干返回值 f(x); return -- 抛弃所以结果 return x or f(x) -- 将结果调整为 1 个元素
2.4.11 函数定义
函数定义的语法如下:
functiondef ::= function funcbody
funcbody ::= ‘(’ [parlist] ‘)’ block end
另外简化定义函数的写法:
stat ::= function funcname funcbody
stat ::= local function Name funcbody
funcname ::= Name {‘.’ Name} [‘:’ Name]
该语句
function f () body end
可以写成这样
f = function () body end
该语句
function t.a.b.c.f () body end
可以写成这样
t.a.b.c.f = function () body end
该语句
local function f() body end
可以写成这样
local f ; f = function() body end
而不是
local f = function() body end
(这个差别只在函数体引用f时才有。)
函数的定义是可执行的表达式,其执行结果是一个类型为 function 的值。当Lua预编译一个代码块时,代码块作为一个函数,整个函数也都会被预编译。那么无论何时Lua执行函数定义,这个函数就进行 实例化 (也称闭包)。这个函数的实例(或称闭包)的最终值是一个表达式。
形参被看作局部变量,它们将由实参的值来实例化:
parlist ::= namelist [‘,’ ‘…’] | ‘…’
当调用函数时,除非是一个不定参函数,即在形参列表末尾处用三个点(…)来注明,否则实参的长度被调整为形参的长度。不定参函数不会调整实参列表长度;取而代之地,它将所有的额外的参数通过 不定参表达式 放在一起,写法依旧三个点。这个表达式的值是一个额外的实参列表,类似一个可以返回多个结果的函数一样。如果一个不定参表达式放在另一个表达式中或在另一个表达式列表中间,那么它的返回值将会被调整为单值。如果不定参表达式放在列表表达式的最后一个,则不进行调整(除非是被括号括起来)。
用个例子来考虑下面的定义:
function f(a, b) end
function g(a, b, …) end
function r() return 1,2,3 end
那么来看看实参到行参和不定参的映射关系:
CALL PARAMETERS
f(3) a=3, b=nil
f(3, 4) a=3, b=4
f(3, 4, 5) a=3, b=4
f(r(), 10) a=1, b=10
f(r()) a=1, b=2
g(3) a=3, b=nil, … –> (nothing)
g(3, 4) a=3, b=4, … –> (nothing)
g(3, 4, 5, 8) a=3, b=4, … –> 5 8
g(5, r()) a=5, b=1, … –> 2 3
结果由 return 来返回(见:§2.3.4)。如果执行函数的末尾都没有遇到 return 语句,那么函数将不会返回结果。
函数返回的结果数目值与系统相关。这个限制一定大于1000。
冒号 语法用来定义 方法 ,也就是说函数会隐含一个参数 self。因此,如下语句
function t.a.b.c:f (params) body end
和下面的写法一样
t.a.b.c.f = function (self, params) body end
2.5 可见性规则
Lua是有词法作用范围的语言。变量的作用范围开始于声明它们之后的第一个语句,结束于这个声明的最内层语句块的最后一块非空语句块。看看下面的例子:
x = 10 -- global variable do -- new block local x = x -- new 'x', with value 10 print(x) --> 10 x = x+1 do -- another block local x = x+1 -- another 'x' print(x) --> 12 end print(x) --> 11 end print(x) --> 10 (the global one)
注意这里,类似 local x = x 这样声明,新的 x 在被声明之前是没有进入作用域的,因此第二个 x 是引用外层的变量。
因此有这样一个词法作用范围规则,在局部变量的作用范围内可以被函数任意使用。一个局部变量被内层的函数使用时,它被内层函数称作 上值,或叫 外部局部变量。
注意,每次执行到一个 local 语句都会定义一个新变量。看看以下例子:
a = {} local x = 20 for i=1,10 do local y = 0 a[i] = function () y=y+1; return x+y end end
这个循环会创建十个闭包(也是十个匿名函数的实例)。这些闭包都会使用不同的 y 变量,而又共享同一个 x 变量。
以上是个人对Lua理解后的翻译,如有任何纰漏请大家多多指教