1 Lua语言入门
1.1 程序段
我们将 Lua 语言执行的每一段代码(例如,一个文件或交互模式下的一行)称为一个程序段 ( Chunk ) ,即一组命令或表达式组成的序列 。
1.2 一些词法规范
Lua 语言中的标识符(或名称)是由任意字母、 数字和下画线组成的字符串(注意,不能以数字开头),例如 :
“下画线 + 大写字母”( 例如 _VERSION )组成的标识符通常被 Lua 语言用作特殊用途,应避免将其用作其他用途。 通常会将“下画线+小写字母”用作哑变量'( Dummy variable )。
以下是 Lua 语言的保留字( reserved word ),它们不能被用作标识符 :
and | break | do | else | elseif |
end | false | goto | for | function |
if | in | local | nil | not |
or | repeat | return | then | true |
until | while |
Lua 语言是对大小写敏感的, 因而虽然 and 是保留字,但是 And 和 AND 就是两个不同的标识符。
Lua 语言中使用两个连续的连字符表示单行注释的开始(从--之后直到此行结束都是注释) , 使用两个连续的连字符加两对连续左方括号表示长注释或多行注释的开始(直到两个连续的右括号为止,中间都是注释),例如 :
--[[ 多行
长注释
]]
在注释一段代码时,一个常见的技巧是将这些代码放入--[[ 和 --]] 之间 ,例如:
--[[
print ( 10 ) -- 无动做(被注释掉了)
--]]
当我们需要重新启用这段代码时,只需在第一行行首添加一个连字符即可(表示该行被单行注释了):
---[[
print(10) --> 10
--]]
在 Lua 语言中,连续语句之间的分隔符并不是必需的,如果有需要的话可以使用分号来进行分隔。 在 Lua 语言中,表达式之间 的换行也不起任何作用 。 例如 , 以下 4 个程序段都是合法且等价的 :
a = 1
b = a * 2
a = 1;
b = a * 2;
a = 1; b = a * 2
a= 1 b=a * 2 -- 可读性不佳,但是却是正确的
1.3 全局变量
在 Lua 语言中,全局变量( Global Variable )无须声明即可使用,使用未经初始化的全局变量也不会导致错误。 当使用未经初始化的全局变量时,得到的结果是 nil :
> b --> nil
> b = 10
> b --> 10
当把 nil 赋值给全局变量时, Lua 会回收该全局变量(就像该全局变量从来没有出现过一样),例如:
> b = nil
> b --> nil
Lua 语言不区分未初始化变量和被赋值为 nil 的变量。 在上述赋值语句执行后, Lua 语言会最终回收该变量占用的内存。
1.4 类型和值
Lua 语言是一种动态类型语言( Dynamically-typed language ),在这种语言中没有类型定义( type definition ),每个值都带有其自身的类型信息。
Lua 语言中有 8 种基本类型: nil (空) 、 boolean (布尔)、 number (数值)、 string (字符串)、 userdata ( 用户数据)、 function (函数)、 thread (线程)和 table (表)。 使用函数 type可获取一个值对应的类型名称:
> type(nil ) --> nil
> type ( true) --> boolean
> type (10.4 * 3) --> number
> type (” Hello world ” ) --> string
> type(io.stdin ) -- > userdata
> type ( print) --> function
> type(type) --> thread(实际测试的结果为function)
> type ( {}) --> table
> type(type(X)) --> string
不管 X 是什么,最后一行返回的永远是string ” 。 这是因为函数 type 返回值永远是一个字符串 。
userdata 类型允许把任意的 C 语言数据保存在 Lua 语言变量中 。 在 Lua 语言中,用户数
据类型除了赋值和相等性测试外,没有其他预定义的操作。 用户数据被用来表示由应用或 C语言编写的库所创建的新类型。 例如,标准 I/O 库使用用户数据来表示打开的文件。 我们会在后面涉及 C API 时再讨论更多的相关内容。
变量没有预定义的类型 ,任何变量都可以包含任何类型的值 :
> type(a) --> nil ( 'a' 尚未初始化)
> a = 10
> type (a) - - > number
> a = ” string”
> type(a) --> string
> a = nil
> type (a ) - -> nil
1.4.1 nil
nil 是一种只有一个 nil 值的类型,它的主要作用就是与其他所有值进行区分。 Lua 语言使用 nil 来表示无效值( non-val ue ,即没有有用的值)的情况。 像我们之前所学习到的,一个
全局变量在第一次被赋值前的默认值就是nil, 而将 nil 赋值给全局变量则相当于将其删除。
1.4.2 Boolean
Boolean 类型具有两个值, true 和 false ,它们分别代表了传统布尔值。 不过,在 Lua 语言 中, Boolean 值并非是用于条件测试的唯一方式 ,任何值都可以表示条件。 在 Lua 语言中,条件测试(例如控制结构中的分支语句)将除 Boolean 值 false 和 nil 外的所有其他值视为真。特别的是,在条件检测中 Lua 语言把零和空字符串也都视为真。
Lua 语言支持常见的逻辑运算符: and 、 or 和 not 。 和条件测试一样,所有的逻辑运算将
Boolean 类型的 false 和 nil 当作假,而把其他值当作真。 逻辑运算符 and 的运算结果为:如果它的第一个操作数为“false”,则返回第一个操作数,否则返回第二个操作数。 逻辑运算符 or 的运算结果为:如果它的第一个操作数不为“false ”,则返回第一个操作数,否则返回第二个操作数。 例如:
> 4 and 5
5
> nil and 13
nil
> false and 13
false
> 0 or 5
0
> false or "hi"
hi
> nil or false
false
>
and 和 or 都遵循短路求值( Short-circuit evaluation )原则,即只在必要时才对第二个操作数进行求值。 例如,根据短路求值的原则,表达式 (i ~ = 0 and a / i > b ) 不会发生运行时异
常(当 i 等于 0 时, a /i 不会执行)。
在 Lua 语言中,形如 x = x or v 的惯用写法非常有用,它等价于 :
if not x then x = v end
即,当 x 未被初始化时,将其默认值设为 v (假设 x 不是 Boolean 类型的 false )。
另一种有用的表达式形如(( a and b) or c )或( a and b or c) (由于 and 的运算符
优先级高于 or ,所以这两种表达形式等价),当 b 不为 false 时,它们还等价于 C 语言的三目运算符 a?b:c 。 例如,我们可以使用表达式( x> y ) and x or y 选出数值x 和 y 中较大的一个。 当 x> y 时, and 的第一个操作数为true ,与第二个操作数(X )进行 and运算后结果为 x ,最终与 or 运算后返回第一个操作数 x 。 当 x> y 不成立时, and 表达式的值为 false ,最终 or 运算后的结果是第二个操作数 y 。
not 运算符永远返回 Boolean 类型的值:
> not nil
true
> not false
true
> not 0
false
> not not 1
true
> not not nil
false
1.5 独立解释器
独立解释器( Stand-alone interpreter ,由于源文件名为 lua.c ,所以也被称为lua.c ;又由
于可执行文件为 lua ,所以也被称为 lua )是一个可以直接使用 Lua 语言的小程序。 这一节介绍它的几个主要参数。
如果源代码文件第一行以井号(#)开头,那么解释器在加载该文件时会忽略这一行。 这
个特征主要是为了方便在 POSIX 系统中将 Lua 作为一种脚本解释器来使用 。 假设独立解释器位于/usr/local/bin 下 ,当使用下列脚本 :
# ! /usr /local/bin/lua
或
#!/usr/bin/env lua
时,不需要显式地调用 Lua 语言解释器也可以直接运行 Lua 脚本。
lua 命令的完整参数形如:
(注意:在windows系统下,使用cmd命令进入终端后可执行下列语句,不能在终端进入Lua交互模式后执行下列语句,否则会报错)
lua [options] [script [args]]
其中,所有的参数都是可选的 。 如前所述,当不使用任何参数调用 lua 时 ,就会直接进入交互模式。
- e 参数允许我们直接在命令行中输入代码,例如:
C:\Users\username>lua -e "print(math.sin(12))"
-0.53657291800043
- l 参数用于加载库。 正如之前提到的那样, -i 参数用于在运行完其他命令行参数后进入交互模式。 因此,下面的命令会首先加载 lib 库,然后执行 x=10 的赋值语句,并最终进入交互式模式:(本人没有安装lib库,所以库加载会失败,可以直接跳过这个 -l 指令)
C:\Users\username>lua -i -l lib -e "x = 10"
如果不想在交互模式中直接输出结果,那么可以在行末加上一个分号:
> io.flush()
true
> io.flush();
>
分号使得最后一行在语法上变成了无效的表达式,但可以被当作有效的命令执行。
我们可以通过预先定义的全局变量 arg 来获取解释器传入的参数。 例如,当执行如下命令时:
C:\Users\username>lua script a b c
[-1] | [0] | [1] | [2] | [3] | [n] |
lua | script | a | b | c | ... |
编译器在运行代码前会创建一个名为 arg 的表,其中存储了所有的命令行参数。 索引 0 中保存的内容为脚本名,索引 1 中保存的内容为第一个参数(本例中的 "a’ ),依此类推;而在脚本之前的所有选项则位于负数索引上,例如 :
C:\Users\username>lua -e "sin=math.sin" script a b
解释器按照如下的方式获取参数:
arg[-3] ="lua"
arg[-2] ="-e"
arg[-1] ="sin=math.sin"
arg[0] ="script"
arg[1] ="a"
arg[2] ="b"
[-3] | [-2] | [-1] | [0] | [1] | [2] |
lua | -e | sin=math.sin | script | a | b |
一般情况下 , 脚本只会用到索引为正数的参数(本例中的 arg[1] 和 arg[2] ) 。
2 小插曲:八皇后问题
要解决八皇后问题,首先必须认识到每一行中只能有一个皇后 。 因此,可以用一个由 8个数字组成的简单数组(一个数字对应一行,代表皇后在这一行的哪一列)来表示可能的解决方案。 例如,数组{ 3, 7, 2, 1, 8, 6, 5 , 4} 表示皇后在棋盘中的位置分别是 (1, 3 ) 、(2, 7 ) 、 ( 3, 2) 、 ( 4, 1 ) 、 ( 5 , 8 ) 、 ( 6, 4) 、 (7 , 5 )和(8, 4) 。 当然 ,这个示例并不是一个正确的解,例如(3, 2)中的皇后就可以攻击 ( 4, 1 )中的皇后 。 此外,我们还必须认识到正确的解必须是整数1到 8 组成的排列( Permutation ),这样才能保证每一列中也只有一个皇后。
示例 2.1 求解八皇后问题的程序
N = 8 --棋盘大小
--检查(n,c)是否不会被攻击
function isplaceok( a,n,c )
for i=1, n-1 do --对于每一个已经被放置的皇后
if (a[i]==c) or --同一列?
(a[i]-i == c-n) or --同一对角线?
(a[i]+i == c+n) then --同一对角线?
return false --位置会被攻击
end
end
return true --不会被攻击,位置有效
end
--打印棋盘
function printsolution( a )
for i=1,N do --对于每一行
for j=1,N do --和每一列
--输出“X”或“-”,外加一个空格
io.write(a[i]==j and "X" or "-", " ")
end
io.write("\n")
end
io.write("\n")
end
--把从‘n’到‘N’的所有皇后放在棋盘‘a’上
function addqueen( a,n )
if n > N then --是否所有的皇后都被放置好了?
printsolution(a)
else --尝试着放置第n个皇后
for c=1,N do
if isplaceok(a,n,c) then
a[n] = c --把第n个皇后放在列‘c’
addqueen(a,n+1)
end
end
end
end
--运行程序
addqueen({},1)
最后一个函数 addqueen 是这段程序的核心,该函数尝试着将所有大于等于 n 的皇后摆放在棋盘上,使用回溯法来搜索正确的解。 首先,该函数检查当前解是否已经完成了所有皇后的摆放,如果已经完成则打印出当前解对应的摆放结果;如果还没有完成,则为第 n 个皇后遍历所有的列,将皇后放置在不会受到攻击的每一列上,并递归地寻找下一个皇后的可能摆放位置。
3 数值
在 Lua5.2及之前的版本中,所有的数值都以双精度浮点格式表示。 从 Lua5.3版本开始,Lua 语言为数值格式提供了两种选择 : 称为 integer 的 64 位整型和被称为 float 的双精度浮点类型。
整型的引人是 Lua 5.3 的一个重要标志,也是与之前版本相比的主要区别 。不过尽管如此 ,由于双精度浮点型能够表示最大为 253 的整型值,所以不会造成太大的不兼容性。
3.1 数值常量
由于整型值和浮点型值的类型都是” number ” ,所以它们是可以相互转换的 。 同时 ,具有相 同算术值的整型值和浮点型值在 Lua 语言中是相等的 :
> 1==1.0
true
> 0.2e3==200
true
> -3==-3.0
true
>
在少数情况下 ,当需要区分整型值和浮点型值时 ,可以使用函数 math.type:
> math.type(3)
integer
> math.type(3.0)
float
Lua 语言像其他语言一样也支持以 h 开头的十六进制常量。 与其他很多编程语言不同 ,Lua 语言还支持十六进制的浮点数,这种十六进制浮点数由小数部分和以 p 或 P 开头的指数部分组成。
> 0xff
255
> 0x1a3
419
> 0x0.2
0.125
> 0x1p-1
0.5
> 0xa.bp2
42.75
计算步骤详解:(以0xa.bp2为例)
- 0x 表示这是一个十六进制数。
- a.b 是尾数部分(mantissa),其中 a 是整数部分,.b 是小数部分。
- p 是指数标记,表示以 2 为底的幂次。
- 2 是指数部分。
步骤1:
尾数部分是 a.b
,需要将其转换为十进制值:
- 十六进制中的
a
等于十进制的10
。 - 十六进制中的
.b
表示
其中,b
的十进制值为11
,因此.b
的值为
所以,尾数部分 a.b 的十进制值为:
步骤2:
指数部分是 2
,表示以 2 为底的幂次:
步骤3:
将尾数部分乘以:
最终结果
0xa.bp2
的十进制数值为 42.75
可以使用%a 参数 , 通过函数 string.format 对这种格式进行格式化输出:
> string.format("%a",419)
0x1.a3p+8
> string.format("%a",0.1)
0x1.999999999999ap-4
虽然这种格式很难阅读,但是这种格式可以保留所有浮点数的精度,并且比十进制的转换速度更快。
3.2 算术运算
除了加、减、乘、除、取负数(单目减法,即把减号当作一元运算符使用)等常见的算术运算外, Lua 语言还支持取整除法( floor 除法)、取模和指数运算。
对于 Lua 5.3 中引人的整型而言,主要的建议就是“开发人员要么选择忽略整型和浮点型二者之间的不同,要么就完整地控制每一个数值的表示 。 ”因此,所有的算术操作符不论操作整型值还是浮点型值,结果都应该是一样的。
如果两个操作数都是整型值,那么结果也是整型值;否则,结果就是浮点型值。 当操作数一
个是整型值一个是浮点型值时, Lua 语言会在进行算术运算前先将整型值转换为浮点型值:
由于两个整数相除的结果并不一定是整数(数学领域称为不能整除),因此除法不遵循上述规则 。 为了避免两个整型值相除和两个浮点型值相除导致不一样的结果,除法运算操作的永远是浮点数且产生浮点型值的结果:
Lua 5.3 针对整数除法引入了一个称为floor除法的新算术运算符 // 。 顾名思义, floor 除法会对得到的商向负无穷取整,从而保证结果是一个整数。 这样,floor 除法就可以与其他算术运算一样遵循同样的规则: 如果操作数都是整型值,那么结果就是整型值,否则就是浮点型值(其值是一个整数)。
> 3//2
1
> 3.0//2
1.0
> 6//2
3
> 6.0//2.0
3.0
> -9//2
-5
> 1.5//0.5
3.0
> 17//2
8
> -3//2
-2
以下公式是取模运算的定义 :
a % b == a - ((a // b) * b)
如果操作数是整数,那么取模运算的结果也是整数。 因此, 取模运算也遵从与算术运算相同的规律, 即如果两个操作数均是整型值, 则结果为整型,否则为浮点型 。
对于整型操作数而言,取模运算的含义没什么特别的,其结果的符号永远与第二个操作数的符号保持一致。 特别地,对于任意指定的正常量 K ,即使 x 是负数,表达式 x%K 的结果也永远在 [ 0, K - 1 ] 之间 。 例如,对于任意整型值 i ,表达式 i%2 的结果均是 0 或 1 。
对于实数类型的操作数而言,取模运算有一些不同 。 例如, x-x%0.01 恰好是 x 保留两位
小数的结果, x-x%0.001 恰好是 x 保留三位小数的结果 :
> x=math.pi
> x-x%0.01
3.14
> x-x%0.001
3.141
再比如, 我们可以使用取模运算检查某辆车在拐过了指定的角度后是否能够原路返回 。假设使用度作为角度的单位,那么我们可以使用如下的公式,该函数对负的角度而言也同样适用 :
local tolerance = 10
function isturnback( angle )
angle = angle % 360
return (math.abs(angle - 180) < tolerance)
end
print(isturnback(-180))
假设使用弧度作为角度的单位,那么我们只需要简单地修改常量的定义即可:
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π)之间 。
Lua 语言同样支持幕运算,使用符号 ^ 表示。 像除法一样,幕运算的操作数也永远是浮
点类型(整型值在幕运算时不能整除,例如, 的结果不是整型值)。 我们可以使用 x^0.5
来计算 x 的平方根,使用 X^(1/3 ) 来计算 x 的立方根。
3.3 关系运算
Lua 语言提供了下列关系运算 :
< > <= >= == ~=
这些关系运算的结果都是 Boolean 类型。
==用于相等性测试,~=用于不等’性测试。 这两个运算符可以应用于任意两个值,当这两
个值的类型不同时, Lua 语言认为它们是不相等的;否则,会根据它们的类型再对两者进行比较。
比较数值时应永远忽略数值的子类型,数值究竟是以整型还是浮点型类型表示并无区别,只与算术值有关(尽管如此,比较具有相同子类型的数值时效率更高)。
3.4 数学库
Lua 语言提供了标准数学库 math 。 标准数学库由一组标准的数学函数组成,包括三角函数( sin 、 cos、tan 、 asin 等)、指数函数、取整函数、最大和最小函数 max 和 min 、用于生
成伪随机数的伪随机数函数(random )以及常量 pi 和 huge (最大可表示数值 ,在大多数平台上代表 inf ) 。
所有的三角函数都以弧度为单位,并通过函数 deg 和「ad 进行角度和弧度的转换。
3.4.1 随机数发生器
函数 math.random 用于生成伪随机数,共有三种调用方式。
方式一: 当不带参数调用时 ,该函数将返回一个在[0, 1 )范围内均匀分布的伪随机实数。
方式二:当使用带有一个整型值 n 的参数调用时 ,该函数将返回一个在[ 1, n]范围内的伪随机整数。
方式三:当使用带有两个整型值 a 和 b 的参数调用时,该函数返回在 [a , b]范围内的伪随机整数。
函数 randomseed 用于设置伪随机数发生器的种子,该函数的唯一参数就是数值类型的种子。 在一个程序启动时,系统固定使用 1 为种子初始化伪随机数发生器。 如果不设置其他的种子,那么每次程序运行时都会生成相同的伪随机数序列 。 从调试的角度看,这是一个不错的特性,然而,对于一个游戏来说却会导致相同的场景重复不断地出现。 为了解决这个问题,通常调用 math . randomseed (os.time()) 来使用当前系统时间作为种子初始化随机数发生器。
3.4.2 取整函数
数学库提供了三个取整函数: floor 、 ceil 和 modf 。 其中, floor 向负无穷取整,ceil
向正无穷取整, modf 向零取整 。 当取整结果能够用整型表示时,返回结果为整型值,否则返回浮点型值(当然,表示的是整数值)。 除了返回取整后的值以外,函数 modf 还会返回小数部分作为第二个结果 。
> math.floor(3.3)
3
> math.floor(-3.3)
-4
> math.ceil(3.3)
4
> math.ceil(-3.3)
-3
> math.modf(3.3)
3 0.3
> math.modf(-3.3)
-3 -0.3
> math.floor(2^70)
1.1805916207174e+021
如果参数本身就是一个整型值,那么它将被原样返回 。
如果想将数值 x 向最近的整数( nearest integer )取整,可以对 x+0.5 调用 floor 函数。 不
过,当参数是一个很大的整数时 ,简单的加法可能会导致错误。 例如,考虑如下的代码:
> x=2^52+1
> print(string.format("%d %d",x,math.floor(x+0.5)))
4503599627370497 4503599627370498
+ 1.5 的浮点值表示是不精确的,因此内部会以我们不可控制的方式取整。 为了避免这个
问题,我们可以单独地处理整数值 :
function round(x)
local f = math.floor(x)
if x == f then return f
else return math.floor(x+0.5)
end
end
上例中的函数总是会向上取整半个整数(例如 2.5 会被取整为 3 )。如果想进行无偏取整( unbiased rounding ),即向距离最近的偶数取整半个整数,上述公式在 x+ 0.5 是奇数的情况下会产生不正确的结果:
> math.floor(3.5+0.5)
4
> math.floor(2.5+0.5)
3
这时,还是可以利用取整操作来解决上述公式中存在的问题:表达式( x% 2.0==0.5 ) 只有在
x+0.5 为奇数时 ( 也就是我们的公式会出错的情况 )为真。 基于这些情况 ,定义一个无偏取整函数就很简单了:
function round(x)
local f = math.floor(x)
if (x == f) or (x %2.0 == 0.5) then
return f
else
return math.floor(x + 0.5)
end
end
print(round(2.5)) ---->2
print(round(3.5)) ---->4
print(round(-2.5)) ---->-2
print(round(-1.5)) ---->-2
3.5 表示范围
标准 Lua 使用 64 个比特位来存储整型值,其最大值为 ,约等于
;精简 Lua
使用 32 个比特位存储整型值,其最大值约为 20 亿。 数学库中的常量定义了整型值的最大值
( math. maxinteger )和最小值( math. mininteger ) 。
64 位整型值中 的最大值是一个很大的数值:全球财富总和(按美分计算)的数千倍和全
球人口总数的数十亿倍。 尽管这个数值很大,但是仍然有可能发生溢出 。 当我们在整型操作时
出现比 mininteger 更小或者 比 maxinteger 更大的数值时 ,结果就会回环 ( wrap round ) 。
在数学领域,回环的意思是结果只能在 mininteger 和 maxinteger 之间,也就是对
取模的算术结果。 在计算机领域,回环的意思是丢弃最高进位( the last carry bit ) 。 假设最高
进位存在,其将是第 65 个比特位,代表 。 因此, 忽略第 65 个比特位不会改变值对
取
模的结果。 在 Lua 语言中,这种行为对所有涉及整型值的算术运算都是一致且可预测的:
> math.maxinteger+1 == math.mininteger
true
> math.mininteger-1 == math.maxinteger
true
> -math.mininteger == math.mininteger
true
> math.mininteger // -1 == math.mininteger
true
最大可以表示的整数是 0x7ff .. . fff 即除最高位(符号位,零为非负数值)外其余比特位均为 1 。 当我们对 0x7ff ... fff 加1时,其结果变为 0x800...000 ,即最小可表示的整数。 最小整数比最大整数的表示幅度大 1 :
> math.maxinteger
9223372036854775807
> 0x7fffffffffffffff
9223372036854775807
> math.mininteger
-9223372036854775808
> 0x8000000000000000
-9223372036854775808
对于浮点数而言 , 标准 Lua 使用双精度。 标准 Lua 使用 64 个比特位表示所有数值,其
中 11 位为指数。 双精度浮点数可以表示具有大致 16 个有效十进制位的数,范围从到
。 精简 Lua 使用 32 个 比特位表示的单精度浮点数,大致具有 7 个有效十进制位,范围
从 到
。
双精度浮点数对于大多数实际应用而言是足够大的,但是我们必须了解精度的限制 。 如果我们使用十位表示一个数,那么 1/7 会被取整到 0.1428571420 如果我们使用十位计算 1/7*7,结果会是 0.999999994 而不是 1 。 此外,用十进制表示的有限小数在用二进制表示时可能是无限小数。 例如, 12.7 -20 + 7.3 即便是用双精度表示也不是 0 ,这是由于 12.7 和 7.3的二进制表示不是有限小数 。
由于整型值和浮点型值的表示范围不同,因此当超过它们的表示范围时,整型值和浮点型值的算术运算会产生不同的结果 :
> math.maxinteger+2
-9223372036854775807
> math.maxinteger+2.0
9.2233720368548e+018
在上例中,两个结果从数学的角度看都是错误的,而且它们错误的方式不同 。 第一行对最大可表示整数进行了整型求和,结果发生了回环。 第二行对最大可表示整数进行了浮点型求和,结果被取整成了一个近似值,这可以通过如下的比较运算证明 :
> math.maxinteger+2.0 == math.maxinteger+1.0
true
尽管每一种表示方法都有其优势,但是只有浮点型才能表示小数。 浮点型的值可以表示很大的范围,但是浮点型能够表示的整数范围被精确地限制在 之间 (不过这个范围已经很大了)。 在这个范围内,我们基本可以忽略整型和浮点型的区别;超出这个范围后,我们则应该谨慎地思考所使用的表示方式。
3.6 惯例
我们可以简单地通过增加 0.0 的方法将整型值强制转换为浮点型值,一个整型值总是可以被转换成浮点型值:
> -3+0.0
-3.0
> 0x7fffffffffffffff+0.0
9.2233720368548e+018
小于 (即 9007199254740992 )的所有整型值的表示与双精度浮点型值的表示一样,对于绝对值超过了这个值的整型值而言,在将其强制转换为浮点型值时可能导致精度损失 :
> 9007199254740991+0.0==9007199254740991
true
> 9007199254740992+0.0==9007199254740992
true
> 9007199254740993+0.0==9007199254740993
false
在最后一行中 ,的结果被取整为
,打破了等式,表达式结果为 false 。
通过与零进行按位或运算,可 以把浮点型值强制转换为整型值 :
> 2^53
9.007199254741e+015 (浮点型值)
> 2^53 | 0
9007199254740992 (整型值)
在将浮点型值强制转换为整型值时, Lua 语言会检查数值是否与整型值表示完全一致,即没有小数部分且其值在整型值的表示范围内,如果不满足条件则会抛出异常:
> 3.2|0 -- 小数部分
stdin:1: number has no integer representation
stack traceback:
stdin:1: in main chunk
[C]: in ?
> 2^64|0 -- 超出范围
stdin:1: number has no integer representation
stack traceback:
stdin:1: in main chunk
[C]: in ?
> math.random(1,3.5)
stdin:1: bad argument #2 to 'random' (数值没有用整型表示)
stack traceback:
[C]: in function 'math.random'
stdin:1: in main chunk
[C]: in ?
对小数进行取整必须显式地调用取整函数。
另一种把数值强制转换为整型值的方式是使用函数 math.tointeger ,该函数会在输入参数无法转换为整型值时返回 nil:
> math.tointeger(-258.0)
-258
> math.tointeger(2^30)
1073741824
> math.tointeger(5.01) (不是整数值)
nil
> math.tointeger(2^64) (超出范围)
nil
这个函数在需要检查一个数字能否被转换成整型值时尤为有用。 例如,以下函数在可能时会将输入参数转换为整型值,否则保持原来的值不变 :
function cond2int( x )
return math.tointeger(x) or x
end
3.7 运算符优先级
Lua 语言中的运算符优先级如下(优先级从高到低):
^ |
一元运算符(- # ~ not) |
* / // % |
+ - |
.. (连接) |
<< >> (按位移位) |
& (按位与) |
~ (按位异或) |
| (按位或) |
< > <= >= ~= == |
and |
or |
在二元运算符中,除了幕运算和连接操作符是右结合的外,其他运算符都是左结合的 。 因此,以下各个表达式的左右两边等价:
4 字符串
Lua 语言中的字符串是一串字节组成的序列, Lua 核心并不关心这些字节究竟以何种方式编码文本。 在 Lua 语言中,字符使用 8 个比特位来存储( eight-bit clean)。 Lua 语言中的字符串可以存储包括空字符在内的所有数值代码,这意味着我们可以在字符串中存储任意的二进制数据。 同样,我们可以使用任意一种编码方法( UTF-8、 UTF-16 等)来存储 Unicode字符串;不过,像我们接下来很快要讨论的那样,最好在一切可能的情况下优先使用UTF-8编码。 Lua 的字符串标准库默认处理 8 个比特位( 1Byte )的字符,但是也同样可以非常优雅地处理 UTF-8 字符串 。 此外,从 Lua 5.3 开始还提供了一个帮助使用 UTF-8 编码的函数库。
Lua 语言中的字符串是不可变值( immutable value ) 。 我们不能像在 C 语言中那样直接改变某个字符串中的某个字符但是我们可以通过创建一个新字符串的方式来达到修改的目的,例如:
> a="one string"
> b=string.gsub(a,"one","another")
> print(a)
one string
> print(b)
another string
像 Lua 语言中的其他对象(表、函数等)一样, Lua 语言中的字符串也是自动内存管理的对象之一。 这意味着 Lua 语言会负责字符串的分配和释放,开发人员无须关注。
可以使用长度操作符( length operator) ( # )获取字符串的长度 :
> a="hello"
> print(#a)
5
> print(#"good bye")
8
该操作符返回字符串占用的字节数,在某些编码中,这个值可能与字符串中字符的个数不同 。
我们可以使用连接操作符 ..(两个点)来进行字符串连接。 如果操作数中存在数值,那么 Lua 语言会先把数值转换成字符串 :
> "hello".."world"
helloworld
> "result is "..3
result is 3
在某些语言中,字符串连接使用的是加号,但实际上 3+5 和 3 .. 5 是不一样的 。
应该注意,在 Lua 语言中,字符串是不可变量。 字符串连接总是创建一个新字符串, 而不会改变原来作为操作数的字符串:
> a="hello"
> a .. "world"
helloworld
> a
hello
4.1 字符串常量
我们可以使用一对双引号或单引号来声明字符串常量( literal string ) :
> a = "a line"
> b= 'another line'
使用双引号和单引号声明字符串是等价的 。 它们两者唯一的区别在于,使用双引号声明的字符串中出现单引号时,单引号可以不用转义;使用单引号声明的字符串中出现双引号时,双引号可以不用转义。
Lua 语言中的字符串支持下列 C 语言风格的转义字符:
\a | 响铃( bell) |
\b | 退格( back space ) |
\f | 换页( form feed) |
\n | 换行( newline) |
\r | 回车( carriage return ) |
\t | 水平制表符( horizontal tab ) |
\v | 垂直制表符( vertical tab ) |
\\ | 反斜杠( backslash ) |
\” | 双引号( double quote ) |
\’ | 单引号( single quote ) |
> print("one line\nnext line\n\"in quotes\",'in quotes'")
one line
next line
"in quotes",'in quotes'
> print('a backslash inside quotes: \'\\\'')
a backslash inside quotes: '\'
> print("a simpler way: '\\'")
a simpler way: '\'
在字符串中,还可以通过转义序列 \ddd 和 \xhh 来声明字符。 其中 ,ddd 是由最多 3 个十
进制数字组成的序列,hh是由两个且必须是两个十六进制数字组成的序列。
从 Lua 5.3 开始,也可以使用转义序列 \u{h...h}来声明 UTF-8 字符,花括号中可以支持任意有效的十六进制:
> ” \ u{3b1} \ u{3b2} \u{3b3}”
α β γ
上例中假定终端使用的是 UTF-8 编码。
4.2 长字符串 / 多行字符串
像长注释/多行注释一样 ,可以使用一对双方括号来声明长字符串/多行字符串常量。 被
方括号括起来的内容可以包括很多行,并且内容中的转义序列不会被转义。 此外,如果多行字符串中的第一个字符是换行符,那么这个换行符会被忽略。 多行字符串在声明包含大段代码的字符串时非常方便,例如:
page = [[
<html>
<head>
<title>An HTML Page</title>
</head>
<body>
<a href="http://www.lua.org">Lua</a>
</body>
</html>
]]
write(page)
有时字符串中可能有类似 a=b[c[i]] 这样的内容(注意其中的 ]] ),或者,字符串中可能有被注释掉的代码。 为了应对这些情况,可以在两个左方括号之间加上任意数量的等号, 如[===[ 。这样,字符串常量只有在遇到了包含相同数量等号的两个右方括号时才会结束(就前例而言,即 ]===])。 Lua 语言的语法扫描器会忽略所含等号数量不相同的方括号。 通过选择恰当数量的等号,就可以在无须修改原字符串的情况下声明任意的字符串常量了。
对注释而言,这种机制也同样有效。 例如,我们可以使用 --[=[ 和]=] 来进行长注释,从而降低了对内部已经包含注释的代码进行注释的难度。
4.3 强制类型转换
Lua 语言在运行时提供了数值与字符串之间的自动转换( conversion ) 。 针对字符串的所有算术操作会尝试将字符串转换为数值。 Lua 语言不仅仅在算术操作时进行这种强制类型转换,还会在任何需要数值的情况下进行,例如函数 math.sin 的参数。
相反,当 Lua 语言发现在需要字符串的地方出现了数值时,它就会把数值转换为字符串 :
> print(10 .. 20)
1020
当在数值后紧接着使用字符串连接时,必须使用空格将它们分开,否则 Lua 语言会把第一个点当成小数点 。
很多人认为自动强制类型转换算不上是 Lua 语言中的一项好设计。 作为原则之一 ,建议最好不要完全寄希望于自动强制类型转换。 虽然在某些场景下这种机制很便利,但同时也给语言和使用这种机制的程序带来了复杂性。
作为这种“二类状态( second-class status )”的表现之一, Lua 5.3 没有实现强制类型转换与整型的集成,而是采用了另一种更简单和快速的实现方式 : 算术运算的规则就是只有在两个操作数都是整型值时结果才是整型。 因此 ,由于字符串不是整型值,所以任何有字符串参与的算术运算都会被当作浮点运算处理:
> "10"+1
11
如果需要显式地将一个字符串转换成数值,那么可以使用函数 tonumber 。 当这个字符串的内容不能表示为有效数字时该函数返回 nil ;否则,该函数就按照 Lua 语法扫描器的规则返回对应的整型值或浮点类型值 :
> tonumber("-3")
-3
> tonumber("10e4")
100000.0
> tonumber("10e")
nil
> tonumber("0x1.3p-4")
0.07421875
默认情况下 ,函数 tonumber 使用的是十进制,但是也可以指明使用二进制到三十六进制之间的任意进制:
> tonumber("100101",2)
37
> tonumber("fff",16)
4095
> tonumber("-ZZ",36)
-1295
> tonumber("987",8)
nil
在最后一行中,对于指定的进制而言,传入的字符串是一个无效值,因此函数 tonumber 返回nil 。
调用函数 tostring 可以将数值转换成字符串:
> print(tostring(10)=="10")
true
上述的这种转换总是有效,但我们需要记住,使用这种转换时并不能控制输出字符串的格式(例如,结果中十进制数字的个数)。 可以通过函数 string.format来全面地控制输出字符串的格式。
与算术操作不同,比较操作符不会对操作数进行强制类型转换。 请注意, “0”和0是不同的 。 此外, 2<15 明显为真,但"2" < "15"却为假(字母顺序)。 为了避免出现不一致的结果,当比较操作符中混用了字符串和数值( 比如 2 < "5")时 , Lua 语言会抛出异常。
4.4 字符串标准库
Lua 语言解释器本身处理字符串的能力是十分有限的。 一个程序能够创建字符串、连接字符串、 比较字符串和获取字符串的长度,但是,它并不能提取字符串的子串或检视字符串的内容。 Lua 语言处理字符串的完整能力来自其字符串标准库。
正如此前提到的,字符串标准库默认处理的是 8 bit ( 1 byte )字符。 这对于某些编码方式(例如 ASCII 或 ISO-8859-1 )适用,但对所有的 Unicode 编码来说都不适用 。 不过尽管如此 ,我们接下来会看到,字符串标准库中的某些功能对 UTF-8 编码来说还是非常有用的 。
字符串标准库中的一些函数非常简单 :函数 string.len(s)返回字符串 s 的长度,等价于#s 。 函数 string.rep(s,n)返回将字符串 s 重复 n 次的结果。 可以通过调用 string.rep (” a ”, 2 ^20 )创建一个 1MB 大小的字符串(例如用于测试)。 函数 string.reverse 用于字符串翻转。 函数 stri ng. lower(s) 返回一份 s 的副本,其中所有的大写字母都被转换成小写字母, 而其他字符则保持不变。 函数 string.upper 与之相反,该函数会将小写字母转换成大写字母。
> string.rep("abc",3)
abcabcabc
> string.reverse("A long Line!")
!eniL gnol A
> string.lower("A Long Line!")
a long line!
> string.upper("A Long Line!")
A LONG LINE!
作为一种典型应用,我们可以使用如下代码在忽略大小写差异的原则下比较两个字符串:
> string.lower(a) < string.lower(b)
true
函数 string.sub(s, i, j) 从字符串 s 中提取第 i 个到第 j 个字符(包括第 i 个和第 j个字符 ,字符串的第一个字符索引为 1 ) 。 该函数也支持负数索引,负数索引从字符串的结尾开始计数 :索引-1代表字符串的最后一个字符 ,索引-2 代表倒数第二个字符,依此类推。 这样,对字符串 s调用函数 string.sub(s, 1, j) 得到的是字符串 s 中长度为 j 的前缀,调用 string.sub(s, j, -1 ) 得到的是字符串 s 中从第 j 个字符开始的后缀,调用 string.sub(s, 2, -2 ) 返回的是去掉字符串 s 中第一个和最后一个字符后的结果:
> s="[in brackets]"
> string.sub(s,2,-2)
in brackets
> string.sub(s,1,1)
[
> string.sub(s,-1,-1)
]
请注意,Lua 语言中的字符串是不可变的 。 和 Lua 语言中的所有其他函数一样,函数
string.sub 不会改变原有字符串的值,它只会返回一个新字符串 。一种常见的误解是以为string.sub(s, 2, -2 )返回的是修改后的 s 。 如果需要修改原字符串,那么必须把新的值赋值给它:
> s = string.sub(s,2,-2)
> print(s)
in brackets
函数 string.char 和 string.byte 用于转换字符及其内部数值表示。 函数 string.char接收零个或多个整数作为参数,然后将每个整数转换成对应的字符,最后返回由这些字符连接而成的字符串 。 函数 string.byte(s, i )返回字符串 s 中第 i 个字符的内部数值表示,该函数的第二个参数是可选的。 调用 string.byte(s)返回字符串 s 中第一个字符(如果字符串只由一个字符组成,那么就返回这个字符)的内部数值表示。 在下例中,假定字符是用ASCII 表示的:
> print(string.char(97))
a
> i=99;print(string.char(i,i+1,i+2))
cde
> print(string.byte("abc"))
97
> print(string.byte("abd",2))
98
> print(string.byte("abc",-1))
99
在最后一行中,使用了负数索引来访问字符串的最后一个字符。
调用 string.byte(s, i, j) 返回索引 i 到 j 之间(包括 i 和 j )的所有字符的数值表示:
> print(string.byte("abc",1,2))
97 98
一种常见的写法是 { string.byte(s, 1, -1 )} ,该表达式会创建一个由字符串 s 中的所有字符代码组成的表(由于 Lua 语言限制了栈大小 ,所以也限制了一个函数的返回值的最大个数,默认最大为一百万个。 因此,这个技巧不能用于大小超过 lMB 的字符串)。
函数 string.format 是用于进行字符串格式化和将数值输出为字符串的强大工具,该函数会返回第一个参数(也就是所谓的格式化字符串 ( formαt string )) 的副本 ,其中的每一个指示符 ( directive )都会被替换为使用对应格式进行格式化后的对应参数。 格式化字符串中的指示符与 C 语言中函数 printf 的规则类似,一个指示符由一个百分号和一个代表格式化方式的字母组成 :d 代表一个十进制整数、 x 代表一个十六进制整数、 f 代表一个浮点数、 s代表字符串,等等。
> string.format("x=%d y=%d",10,20)
x=10 y=20
> string.format("x=%x",200)
x=c8
> string.format("x=0x%X",200)
x=0xC8
> string.format("x=%f",200)
x=200.000000
> tag,title="h1","a title"
> string.format("<%s>%s</%s>",tag,title,tag)
<h1>a title</h1>
在百分号和字母之间可以包含用于控制格式细节的其他选项。 例如, 可以指定一个浮点数中小数点的位数 :
> print(string.format("pi=%.4f",math.pi))
pi=3.1416
> d=5;m=11;y=1990
> print(string.format("%02d/%02d/%04d",d,m,y))
05/11/1990
在上例中,% .4f 表示小数点后保留 4 位小数 ;%02d 表示一个十进制数至少由两个数字组成,不足两个数字的用 0 补齐,而%2d 则表示用空格来补齐。 关于这些指示符的完整描述可以参阅 C 语言 printf 函数的相关文挡,因为 Lua 语言是通过调用 C 语言标准库来完成实际工作的。
可以使用冒号操作符像调用字符串的一个方法那样调用字符串标准库中的所有函数。 例如 , string.sub(s, i, j) 可以重写为 s:sub(i, j), string.upper(s)可以重写为 s:upper()。
字符串标准库还包括了几个基于模式匹配的函数。 函数 string.find 用于在指定的字符串中进行模式搜索 :
> string.find("hello world","wor")
7 9
> string.find("hello world", "war")
nil
如果该函数在指定的字符串中找到了匹配的模式, 则返回模式的开始和结束位置,否则返回nil 。 函数 string.gsub ( Global SUBstitution ) 则把所有匹配的模式用另一个字符串替换 :
> string.gsub("hello world","l",".")
he..o wor.d 3
> string.gsub("hello world","ll","..")
he..o world 1
> string.gsub("hello world","a",".")
hello world 0
该函数还会在第二个返回值中返回发生替换的次数。