第三章 表达式
表达式用于获取值或者设置值。Lua中的表达式包括数字常量、字面字符串、变量、一元和二元操作符及函数调用等。与常规不同的是,表达式中也可以包含函数定义和表结构。
3.1 算术运算符
Lua支持常见的算术运算符:二元运算符 '+'(加)、'-'(减)、'*'(乘)、'/'(除)、'^'(指数)、‘%’(取模)以及一元运算符 ‘-’(取反),所有运算符的操作对象必须是实数。比如x^0.5计算x的平方根,x^(-1/3)计算x倒数的立方根。
下面的式子是取模运算的 定义:
a % b == a - math.floor(a/b)*b
当对整数进行取模运算时,意义跟通常说的取余一样,结果总是跟第二个参数的正负保持一致。但当扩展到实数域的时候,取模运算就有了额外的用途。比如,x%1是取x的小数部分;x-x%1是取它的整数部分;x-x%0.01是将x精确到两位小数:
x = math.pi
print(x - x%0.01) --> 3.14
再举一个使用取模运算的例子。假设想验证一辆车是否在绕行一定角度之后处于掉头状态,如果角的单位使用度(°),那么可以使用下面的代码:
local tolerance = 10
function isturnback (angle)
angle = angle % 360
return (math.abs(angle - 180) < tolerance)
end
这段代码对于负数角度依旧有效:
print(isturnback(-180)) --> true
如果想用弧度替代角度,只需要如下更改代码:
local tolerance = 0.17
function isturnback (angle)
angle = angle % (2*math.pi)
return (math.abs(angle - math.pi) < tolerance)
end
语句 angle%(2*math.pi) 可以将任何角度转化成位于[0,2π]区间的弧度。
3.2 关系运算符
Lua支持下面列出的关系运算符:
< > <= >= == ~=
所有这些运算符的运算结果一定是一个布尔值。
"=="用于测试是否相等;"~="用于测试是否不等,这两个运算符可以接受任意两个值,Lua会认为类型不一样的两个值不相等。如果类型相等,Lua会根据他们的类型选择比较方式。特别需要指出的是,nil只跟它自身相等。
如果变量的类型是表或者自定义类型,那么判断变量是否相等只是判断变量是否是同一个对象的引用。参考下面代码的例子:
a = {}; a.x = 1; a.y = 0
b = {}; b.x = 1; b.y = 0
c = a
print(a==b) -->false
print(a==c) -->true
只能比较两个数字或者字符串的大小。Lua按照字母表排序进行字符串的比较,具体的排序取决于区域设置。比如将区域设置为"葡萄牙 拉丁语-1",那么 "acai"<"açaí"<"acorde"。除了数字和字符串,其它类型的值只能比较是否相等,不能比较大小。
当用不同类型的值作比较的时候,一定要小心谨慎:字符串”0“和数字0是不同的;并且2<15显然是true,但"2"<"15"是false(是按照字母排序)。为了帮助避免产生这种与预期不一致的结果,当用字符串跟数字比较的时候,Lua会提示错误,比如2<"15"。
3.3 逻辑运算符
逻辑运算符包括
and ,or,not 。 同在控制结构中类似,所有的逻辑运算符将fasle 和nil都认为是假,其它值认为是真。and 运算符在第一个参数为假的时候返回第一个参数,否则返回第二个参数。or运算符在第一个参数为真的时候返回第一个参数,否则返回第二个参数:
print(4 and 5) --> 5
print(nil and 13) --> nil
print(false and 13) --> false
print(4 or 5) --> 4
print(false or 5) --> 5
and和or使用"短路求值"的方法,也就是说,只有在必要的时候这两个运算符才会去对第二个操作数进行估值。"短路求值"可以保证类似(type(v)=="table" and v.tag=="h1")的表达式不会引发运行时错误:当v不是一个表的时候,Lua就不会再去尝试获取v.tag的值。
Lua中的一个常见用法是x=x or v,等价于:
if not x then
x = v
end
也就是说,当x的值没有设置的时候,给它设置一个默认值v(能够确保x的值不是false )。
另一个常见用法是(a and b) or c,因为and 的优先级比 or 高,所以也可以简单写做a and b or c,在b不是假的前提下,它等效于C表达式a?b:c。比如,可以这样得到两个数字x和y中较大的那一个:
max = (x > y) and x or y
当x>y的时候,and 的结果是x,因为x为真,所以 or 返回x,max被赋值为x;当x<y时,and 的结果返回false ,所以or 返回第二个参数y,max被赋值为为y。
not操作符总是返回一个布尔类型的值:
print(not nil) --> true
print(not false) --> true
print(not 0) --> false
print(not not 1) --> true
print(not not nil) --> false
3.4 字符串串联
Lua将".."(两个点)定义为字符串串联运算符,如果操作数是数字,Lua会自动把数字转换成字符串(有些语言使用'+'作为串联运算符,但3+5和3 ..5是不同的)
print("Hello " .. "World") --> Hello World
print(0 .. 1) --> 01
print(000 .. 01) --> 01
Lua中的字符串值是不可更改的,串联操作总是会创建一个新字符串,并不会对它的操作数做任何修改:
a = "Hello"
print(a .. " World") --> Hello World
print(a) --> Hello
3.5 长度运算符
长度运算符对字符串和表类型有效:它会返回字符串中的比特数或者用表实现的序列的长度。
使用长度运算符操作序列有这么几种常见用法:
print(a[#a]) -- prints the last value of sequence 'a'
a[#a] = nil -- removes this last value
a[#a + 1] = v -- appends 'v' to the end of the list
在最后一章将会讨论到,使用长度运算符计算有"洞"(空值)列表的长度,结果是不可预知的。它只对序列有效——没有"洞"的列表。更准确的说,序列是以数字作为键名的表,这些键名从1开始,以1递增(请牢记,实际上,表中不可能存在空值)。需要指出的是,键名类型均不是数字的表是一个长度为0的序列。
多年以来,关于扩充对有洞列表的长度运算符有各种各样的提议,但都是说起来容易做起来难。问题的关键是,"列"的本质是"表",讨论它的"长度"往往意义模糊。可以思考一下下面的例子:
a = {}
a[1] = 1
a[2] = nil -- 其实什么事情都没做,因为a[2]本来就是nil
a[3] = 1
a[4] = 1
或许可以简单的认为这个列表的长度是4,虽然它在索引为2的位置有一个洞,那么,下面这个例子该如何解释?
a = {}
a[1] = 1
a[10000] = 1
可以把它认为是一个有10000个元素,但有9998个都是nil的列表吗?那如果程序再这样做:
a[10000] = nil
那么,现在这个列表的长度是多少?因为它只删掉了最后一个元素,所以长度是9999吗?或者依旧是10000,程序仅仅是将最后一个元素的值设置成了nil:或者长度瓦解为1?
还有一个常见的提议是让#运算符返回一个表中所有元素的数量,这个语法看起来简单并且容易实现,但问题是,这样做根本没有实际意义。结合之前所有的例子想一下,这样一个运算符在对现实中处理列表或者数组的算法能起到怎样的作用?
更让人困惑的是列表结尾的nil该如何处理?下面列表的长度应该是多少呢:
a = {10, 20, 30, nil, nil}
需要记住一点的是,对于Lua来说,一个值为nil的区域和"不存在"的区域之间界限模糊。因此,上面的表a等价于{10. 20. 30},它的长度是3,而不是5。
可能有人会说,结尾是nil的列表是非常特殊的情况,根本不用这么纠结。但是,在现实使用中,列表往往是通过不断一个接一个的累积才构建出最终的形态,所有通过这种形式得到的有洞列表,肯定都曾有过结尾是nil的情况。
在程序中使用的多数列表都是序列(比如文件的任何一行都不能是nil),因此,多数时候使用长度运算符是安全的。但对于有洞列表必须特殊对待,应当在某个位置显式声明它的长度,而不能指望长度运算符一定能得到你期望的结果。
3.6 优先级
Lua中的运算符优先级遵从下面的表格,按照从高到到低排列
^
not # -(一元取反)
\* / %
\+ -
..
< > <= >= ~= ==
and
or
除了'^'(指数运算符)和'..'(串联运算符),其它所有的二元运算符都是左结合的。下面的例子中,左边和邮编的表达式等价:
a+i < b/2+1 <--> (a+i) < ((b/2)+1)
5+x^2*8 <--> 5+((x^2)*8)
a < y and y <= z <--> (a < y) and (y <= z)
-x^2 <--> -(x^2)
x^y^z <--> x^(y^z)
当对优先级有疑问的时候,一定要使用括号来显式指明运算关系,这比查找参考手册简单的多,而且,下次重读代码的时候,你可能依旧会有疑问。
3.7 表构造器
构造器是一种用于创建和初始化表格的表达形式,它是Lua的独有功能,也是Lua最有用和最实用的功能之一。
最简单的构造器是空构造器{},正如之前用过的那样,它可以创建一个空表。构造器在创建的同时也
可以初始化表,比如下面的声明:
days = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday","Friday", "Saturday"}
可以将days[1]用字符串"Sunday"初始化(用构造器初始化的表中,第一个元素的索引是1,不是0),days[2]用"Monday",以此类推:
print(days[4]) --> Wednesday
Lua同样提供一种初始化类似记录形式的表的特殊方法,正如下面的例子:
a = {x=10, y=20}
上一行代码与下面等价:
a = {}; a.x=10; a.y=20
第一种表达方法更快一些,因为Lua在创建表的时候已经知道了它的实际大小。
不管是用哪种方法创建和初始化了一个表,都可以任意在结果中增删元素:
w = {x=0, y=0, label="console"}
x = {math.sin(0), math.sin(1), math.sin(2)}
w[1] = "another field" -- 向表'w'中增加键1
x.f = w -- 向表'w'中增加键"f"
print(w["x"]) --> 0
print(w[1]) --> another field
print(x.f[1]) --> another field
w.x = nil -- 从表'w'中移除键"x"
但正如刚刚提到的,使用非空构造器初始化表不仅更高效,看起来也更优雅。
在同一个构造器中可以混用两种格式对表进行初始化:
polyline = {
color="blue",
thickness=2,
npoints=4,
{x=0, y=0}, -- polyline[1]
{x=-10, y=0}, -- polyline[2]
{x=-10, y=1}, -- polyline[3]
{x=0, y=1} -- polyline[4]
}
上面的例子也在说明如何嵌套使用构造器来创建一个能够表示复杂数据结构的表。polyline[i]的每个
元素都是一个表示记录的表:
print(polyline[2].x) --> -10
print(polyline[4].y) --> 1
这两种构造器都有它们的局限性,比如无法使用负数作为数字索引,也无法使用不合法标识符作为字符串索引。为了满足要求,Lua中还有一种更通用的格式。在这种格式中,可以使用方括号赋值语句来对索引进行显示的初始化:
opnames = {["+"] = "add", ["-"] = "sub",["*"] = "mul", ["/"] = "div"}
i = 20; s = "-"
a = {[i+0] = s, [i+1] = s..s, [i+2] = s..s..s}
print(opnames[s]) --> sub
print(a[22]) --> --
这种语法更复杂,也更灵活,其它两种形式都是折翼通用语法的特殊情况。比如 {x=0,y=0}等价于:{["x"]=0,["y"]=0, {"r","g","b"等价于{[1]="r",[2]="g",[3]="b"}。
通用构造器构造一个表的时候,最后一个大括号前可以加一个分号,这个尾部的分号可选,但不影响使用:
a = {[1]="red", [2]="green", [3]="blue",}
这种设定使得那些能够自动生成Lua构造器代码的程序不需要特殊处理最后一个元素。
最后,在构造器中,逗号和分号是可以任意替代的。通常习惯使用分号作为构造器中不同部分间的分割,比如将列表部分和记录部分区分开:
{x=10, y=45; "one", "two", "three"}
练习
练习3.1:下面的代码会打印什么内容?
for i = -10, 10 do
print(i, i % 3)
end
练习3.2 表达式 2^3^4的结果是什么?2^-3^4呢?
练习3.3: