3.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 ‘)’
数字和字面串在 §3.1 中解释;变量在 §3.2 中解释;函数定义在 §3.4.11 中解释;函数调用在 §3.4.10 中解释;表的构造在 §3.4.9 中解释。可变参数的表达式写作三个点('…
'),它只能在有可变参数的函数中直接使用;这些在 §3.4.11 中解释。
二元操作符包含有数学运算操作符(参见 §3.4.1),位操作符(参见 §3.4.2),比较操作符(参见 §3.4.4),逻辑操作符(参见 §3.4.5),以及连接操作符(参见 §3.4.6)。一元操作符包括负号(参见 §3.4.1),按位非(参见 §3.4.2),逻辑非(参见 §3.4.5),和取长度操作符(参见 §3.4.7)。
函数调用和可变参数表达式都可以放在多重返回值中。 如果函数调用被当作一条语句(参见 §3.3.6),其返回值列表被调整为零个元素,即抛弃所有的返回值。如果表达式被用于表达式列表的最后(或是唯一的)一个元素,那么不会做任何调整(除非表达式被括号括起来)。在其它情况下,Lua 都会把结果调整为一个元素置入表达式列表中,即保留第一个结果而忽略之后的所有值,或是在没有结果时,补单个 nil。
这里有一些例子:
- f() -- 调整为 0 个结果
- g(f(), x) -- f() 会被调整为一个结果
- g(x, f()) -- g 收到 x 以及 f() 返回的所有结果
- a,b,c = f(), x -- f() 被调整为 1 个结果 (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 ... -- 返回从可变参数列表中接收到的所有参数parameters
- return x,y,f() -- 返回 x, y, 以及 f() 的所有返回值
- {f()} -- 用 f() 的所有返回值创建一个列表
- {...} -- 用可变参数中的所有值创建一个列表
- {f(), nil} -- f() 被调整为一个结果
被括号括起来的表达式永远被当作一个值。所以,(f(x,y,z))
即使 f
返回多个值,这个表达式永远是一个单一值。 ((f(x,y,z))
的值是 f
返回的第一个值。如果 f
不返回值的话,那么它的值就是 nil 。)
3.4.1 – 数学运算操作符
Lua 支持下列数学运算操作符:
+
: 加法-
: 减法*
: 乘法/
: 浮点除法//
: 向下取整除法%
: 取模^
: 乘方-
: 取负 除了乘方和浮点除法运算,数学运算按如下方式工作:如果两个操作数都是整数,该操作以整数方式操作且结果也将是一个整数。否则,当两个操作数都是数字或可以被转换为数字的字符串(参见 §3.4.3)时,操作数会被转换成两个浮点数,操作按通常的浮点规则(一般遵循 IEEE 754 标准)来进行,结果也是一个浮点数。
乘方和浮点除法 (/
)总是把操作数转换成浮点数进行,其结果总是浮点数。乘方使用 ISO C 函数 pow
,因此它也可以接受非整数的指数。
向下取整的除法 (//
)指做一次除法,并将商圆整到靠近负无穷的一侧,即对操作数做除法后取 floor 。
取模被定义成除法的余数,其商被圆整到靠近负无穷的一侧(向下取整的除法)。
对于整数数学运算的溢出问题,这些操作采取的策略是按通常遵循的以 2 为补码的数学运算的 环绕 规则。(换句话说,它们返回其运算的数学结果对 264 取模后的数字。)
3.4.2 – 位操作符
Lua 支持下列位操作符:
&
: 按位与|
: 按位或~
: 按位异或>>
: 右移<<
: 左移~
: 按位非 所有的位操作都将操作数先转换为整数(参见 §3.4.3),然后按位操作,其结果是一个整数。
对于右移和左移,均用零来填补空位。移动的位数若为负,则向反方向位移;若移动的位数的绝对值大于等于整数本身的位数,其结果为零(所有位都被移出)。
3.4.3 – 强制转换
Lua 对一些类型和值的内部表示会在运行时做一些数学转换。位操作总是将浮点操作数转换成整数。乘方和浮点除法总是将整数转换为浮点数。其它数学操作若针对混合操作数(整数和浮点数)将把整数转换为浮点数;这一点被称为 通常规则。C API 同样会按需把整数转换为浮点数以及把浮点数转换为整数。此外,字符串连接操作除了字符串,也可以接受数字作为参数。
当操作需要数字时,Lua 还会把字符串转换为数字。
当把一个整数转换为浮点数时,若整数值恰好可以表示为一个浮点数,那就取那个浮点数。否则,转换会取最接近的较大值或较小值来表示这个数。这种转换是不会失败的。
将浮点数转为整数的过程会检查浮点数能否被准确的表达为一个整数(即,浮点数是一个整数值且在整数可以表达的区间)。如果可以,结果就是那个数,否则转换失败。
从字符串到数字的转换过程遵循以下流程:首先,遵循按 Lua 词法分析器的规则分析语法来转换为对应的整数或浮点数。(字符串可以有前置或后置的空格以及一个符号。)然后,结果数字再按前述规则转换为所需要的类型(浮点或整数)。
从数字转换为字符串使用非指定的人可读的格式。若想完全控制数字到字符串的转换过程,可以使用字符串库中的 format
函数(参见 string.format
)。
3.4.4 – 比较操作符
Lua 支持下列比较操作符:
==
: 等于~=
: 不等于<
: 小于>
: 大于<=
: 小于等于>=
: 大于等于 这些操作的结果不是 false 就是 true。
等于操作 (==
)先比较操作数的类型。如果类型不同,结果就是 false。否则,继续比较值。 字符串按一般的方式比较。数字遵循二元操作的规则:如果两个操作数都是整数,它们按整数比较;否则,它们先转换为浮点数,然后再做比较。
表,用户数据,以及线程都按引用比较:只有两者引用同一个对象时才认为它们相等。每次你创建一个新对象(一张表,一个用户数据,或一个线程),新对象都一定和已有且存在的对象不同。相同引用的闭包一定相等。有任何可察觉的差异(不同的行为,不同的定义)一定不等。
你可以通过使用 "eq" 元方法(参见 §2.4)来改变 Lua 比较表和用户数据时的方式。
等于操作不会将字符串转换为数字,反之亦然。即,"0"==0
结果为 false,且 t[0]
与 t["0"]
指代着表中的不同项。
~=
操作完全等价于 (==
) 操作的反值。
大小比较操作以以下方式进行。如果参数都是数字,它们按二元操作的常规进行。否则,如果两个参数都是字符串,它们的值按当前的区域设置来比较。再则,Lua 就试着调用 "lt" 或是 "le" 元方法(参见 §2.4)。a > b
的比较被转译为 b < a
,a >= b
被转译为 b <= a
。
3.4.5 – 逻辑操作符
Lua 中的逻辑操作符有 and, or,以及 not。和控制结构(参见 §3.3.4)一样,所有的逻辑操作符把 false 和 nil 都作为假,而其它的一切都当作真。
取反操作 not 总是返回 false 或 true 中的一个。与操作符 and 在第一个参数为 false 或 nil 时返回这第一个参数;否则,and 返回第二个参数。 或操作符 or 在第一个参数不为 nil 也不为 false 时,返回这第一个参数,否则返回第二个参数。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
(在这本手册中,—>
指前面表达式的结果。)
3.4.6 – 字符串连接
Lua 中字符串的连接操作符写作两个点('..
')。如果两个操作数都是字符串或都是数字,连接操作将以 §3.4.3 中提到的规则把其转换为字符串。否则,会调用元方法 __concat
(参见 §2.4)。
3.4.7 – 取长度操作符
取长度操作符写作一元前置符 #
。字符串的长度是它的字节数(就是以一个字符一个字节计算的字符串长度)。
程序可以通过 __len
元方法(参见 §2.4)来修改对字符串类型外的任何值的取长度操作行为。
如果 _len
元方法没有给出,表 t
的长度只在表是一个 序列 时有定义。序列指表的正数键集等于 {1..n} ,其中 _n 是一个非负整数。在这种情况下,n 是表的长度。注意这样的表
- {10, 20, nil, 40}
不是一个序列,因为它有键 4
却没有键 3
。(因此,该表的正整数键集不等于 {1..n} 集合,故而就不存在 n。)注意,一张表是否是一个序列和它的非数字键无关。
3.4.8 – 优先级
Lua 中操作符的优先级写在下表中,从低到高优先级排序:
- or
- and
- < > <= >= ~= ==
- |
- ~
- &
- << >>
- ..
- + -
- * / // %
- unary operators (not # - ~)
- ^
通常,你可以用括号来改变运算次序。连接操作符 ('..
') 和乘方操作 ('^
') 是从右至左的。 其它所有的操作都是从左至右。
3.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
,而且其表达式是一个函数调用或者是一个可变参数,那么这个表达式所有的返回值将依次进入列表 (参见 §3.4.10)。
初始化域表可以在最后多一个分割符,这样设计可以方便由机器生成代码。
3.4.10 – 函数调用
Lua 中的函数调用的语法如下:
- functioncall ::= prefixexp args
函数调用时,第一步,prefixexp 和 args 先被求值。如果 prefixexp 的值的类型是 function,那么这个函数就被用给出的参数调用。否则 prefixexp 的元方法 "call" 就被调用,第一个参数是 prefixexp 的值,接下来的是原来的调用参数 (参见 §2.4)。
这样的形式
- functioncall ::= prefixexp ‘:’ Name args
可以用来调用 "方法"。这是 Lua 支持的一种语法糖。像 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)) -- 返回值被调整为一个
- return 2 * f(x)
- return x, f(x) -- 追加若干返回值
- f(x); return -- 返回值全部被舍弃
- return x or f(x) -- 返回值被调整为一个
3.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 来返回(参见 §3.3.4)。如果执行到函数末尾依旧没有遇到任何 return 语句,函数就不会返回任何结果。
关于函数可返回值的数量限制和系统有关。这个限制一定大于 1000 。
冒号 语法可以用来定义 方法,就是说,函数可以有一个隐式的形参 self
。因此,如下语句
- function t.a.b.c:f (params) body end
是这样一种写法的语法糖
- t.a.b.c.f = function (self, params) body end