用一篇文章让你学会脚本语言 Lua
楔子
在介绍微服务的时候,我们提到过网关,它是流量的入口。而提到网关,那么就离不开 Lua 脚本,因此为了后续学习网关能够更加方便,本篇文章先来一起学习一下 Lua 这门语言。
Lua 是一门脚本语言,解释器由 C 语言编写,源码总共两万多行,可以说非常的少,就冲源代码的数量,就决定了这门语言难不倒哪里去。不过也正因为 Lua 的精简性,导致它无法独立地开发大型应用,Lua 存在的意义是为了给现有组件提供扩展功能,像 Redis、Nginx 等都支持嵌入 Lua 脚本。
下面我们就开始吧。
Lua 的安装
首先来安装 Lua,这里安装最新的 5.4 版本,我们进入 Lua 的官网,根据操作系统选择合适的安装包即可,我这里是 Windows。
下载完之后,直接解压即可,目录里面就 4 个文件。
这几个文件的作用如下:
- luac54.exe:Lua 编译器的可执行文件,负责将 Lua 脚本文件编译成字节码文件(后缀名为 .luac)。字节码文件可以更快地加载和执行,因此在发布阶段可以使用它们来保护源代码和提高执行效率。所以 Lua 虽然是解释型语言,但也是要经过编译的。而 Lua 解释器由一个 Lua 编译器加上一个 Lua 虚拟机组成,Lua 编译器将 .lua 文件编译成 .luac 文件之后,会交给 Lua 虚拟机执行。
- lua54.exe: Lua 解释器的可执行文件,我们通过它来执行 Lua 脚本(后缀名为 .lua),或者通过交互模式运行 Lua 代码。当然在执行 Lua 代码之前,会先通过 Lua 编译器将其编译成字节码。只不过编译器的实现没有和虚拟机集成在一起,而是单独抽离成了 luac54.exe(会在编译 Lua 代码时被 lua54.exe 所调用)。
- lua54.dll: Lua 虚拟机依赖的动态链接库(DLL)文件,它提供了运行 Lua 脚本的功能,其它程序可以通过加载这个 DLL 文件来嵌入 Lua 解释器,从而执行 Lua 脚本。
- wlua54.exe:这是 Windows 平台上 Lua 解释器的可执行文件,它与 lua54.exe 的功能相同,但具有 Windows 特定的支持和接口,wlua54.exe 可以用于在 Windows 系统上执行 Lua 脚本,我们直接用 lua54.exe 即可。
可以看到 luac54.exe、lua54.exe、lua54.dll 组合起来,便构成了整个 Lua 解释器,而大小加起来还不到 800K,所以引入 Lua 扩展之后的应用程序,大小几乎没有变化,但是却极大地方便了程序的开发。特别是在游戏领域,对于端游来说,脚本还算是锦上添花的东西,但对于手游来说,脚本就是刚需了。
然后将该目录设置到环境变量中,通过在终端中输入 lua54,即可进入到 Lua 的交互式环境中。这里我就不演示了,下面会使用 IDE 进行操作,关于 IDE,我这里使用的是 Goland,下载一个 Lua 的插件即可。下面老规矩,我们来打印一个 "hello world",这个仪式感不能丢。创建一个 main.lua,里面写上一行打印语句。
print("Hello World")
执行的时候,在控制台就会显示出 Hello World。估计有小伙伴觉得,你这写的是 Python 吧,其实 Lua 中的打印函数也是 print,并且也是内置的,并且 Lua 中的字符串可以使用单引号、也可以使用双引号,当然三引号不可以。
Lua 是怎么执行代码的?
这部分内容感兴趣了解一下就好,对我们后续使用 Lua 开发来说没有影响。
在 Lua 的行话里,一段可以被 Lua 解释器解释执行的代码就叫作 chunk。因此 chunk 可以很小,小到只有一两条语句,也可以很大,大到包含成千上万条语句和复杂的函数定义。前面提到过,为了获得较高的执行效率,Lua 并不是直接解释执行 chunk,而是先由编译器编译成内部结构(其中包含字节码等信息),然后再由虚拟机执行字节码。这种内部结构在 Lua 里就叫作预编译(Precompiled)chunk,由于采用了二进制格式,所以也叫二进制(Binary)chunk。
我们以 Java 和 Python 作为对照,存放 chunk 的源文件(一般以 .lua 为后缀)对应 .java 源文件和 .py 源文件,二进制 chunk 则对应编译好的 .class 文件和 .pyc 文件。Java 的 .class 文件和 Python 的 .pyc 文件里面除了字节码外,还有常量池、符号表等信息,类似地,二进制 chunk 里也有这些信息。当然啦,Lua 程序员一般不需要关心二进制 chunk,因为 Lua 解释器会在内部进行编译。
假设当前有一个 main.lua 文件,我们直接通过 lua54 main.lua 即可执行,内部会自动编译。当然啦,我们也可以先编译,再执行。
luac54 -o main.luac main.lua
lua54 mian.luac
先手动将 main.lua 编译成 main.luac,然后 luc54.exe 直接执行 main.luac 即可。
需要说明的是,在 Lua 5.3 以及之前的版本,编译器的名字就叫 luac.exe,解释器的名字叫 lua.exe。但我当前下载的版本,里面带上了 54,表示具体版本。后续为了方便,有时候会把 luac54 说成 luac,把 lua54 说成 lua,不过名字什么的无所谓,大家知道就好。
然后再来说一说 luac,它主要有两个用途:第一,作为编译器,把 Lua 源文件编译成二进制 chunk 文件:第二,作为反编译器,分析二进制 chunk,将信息输出到控制台。
编译 Lua 源文件
将一个或者多个文件名作为参数调用 luac 命令就可以编译指定的 Lua 源文件,如果编译成功,在当前目录下会出现 luac.out 文件,里面的内容就是对应的二进制 chunk。如果不想使用默认的输出文件,可以使用 -o 选项对输出文件进行明确指定。
luac54 main.lua # 生成 luac.out
luac54 -o main.luac main.lua # 生成 main.luac
编译生成的二进制 chunk 默认包含调试信息(行号、变量名等),可以使用 -s 选项告诉 luac 去掉调试信息。另外,如果仅仅想检查语法是否正确,不想产生输出文件,可以使用 -p 选项进行编译。
luac54 -s main.lua # 生成 luac.out,不包含调试信息
luac54 -s -o main.luac main.lua # 生成 main.luac,不包含调试信息
luac54 -p main.lua # 只进行语法检查
Lua 编译器以函数为单位进行编译,每一个函数都会被 Lua 编译器编译为一个内部结构,这个结构叫作原型(Prototype)。原型主要包含 6 部分内容,分别是:函数基本信息(包括参数数量、局部变量数量等)、字节码、常量表、Upvalue 表、调式信息、子函数原型列表。由此可知,函数原型是一种递归结构,并且 Lua 源码中函数的嵌套关系会直接反映在编译后的原型里。
细心的你一定会想到这样一个问题:main.lua 里面只有一条打印语句,并没有定义函数,那么 Lua 编译器是怎么编译这个文件的呢?由于 Lua 是脚本语言,如果我们每执行一段脚本都必须要定义一个函数,岂不是很麻烦?所以这个吃力不讨好的工作就由 Lua 编译器代劳了。
Lua 编译器会自动为我们的脚本添加一个 main 函数(即主函数),并且把整个程序都放进这个函数里,然后再以它为起点进行编译,那么自然就把整个程序都编译出来了。这个主函数不仅是编译的起点,也是未来 Lua 虚拟机解释执行程序时的入口。而我们写的 Hello World 程序被 Lua 编译器加工之后,就变成了下面这个样子。
function main()
print("Hello World")
return
end
把主函数编译成函数原型后,Lua 编译器会给它再添加一个头部(Header),然后一起 dump 成 luac.out 文件,这样一份热乎的二进制chunk文件就新鲜出炉了。
如果对上面这些内容不是太理解也没关系,我们只是从宏观的角度上简单聊一下,它对我们后续学习 Lua 并无太大影响。
查看二进制 chunk
二进制 chunk 之所以使用二进制格式,是为了方便虚拟机加载,然而对人类却不够友好,因为其很难直接阅读。而 luac 命令兼具编译和反编译功能,使用 -l 选项可以将 luac 切换到反编译模式。
luac54 -l main.luac
反编译的结果如下:
初识 Lua
任何一门语言都提供了不同类型的数据结构,那么 Lua 中都有哪些数据结构呢?
nil:空
boolean:布尔类型,分别是 true 和 false
number:数值型,整型和浮点型都属于 number
string:字符串
table:表
function:函数
userdata:用户数据
thread:线程
Lua 总共提供了以上 8 种数据类型,目前只需要知道一下即可,后面会一点一点介绍。
然后是 Lua 的关键字,总共有 22 个,如下所示。
and break do else elseif end false
goto for function if in local nil
not or repeat return then true until while
这些关键字显然基本上都见过,后续会慢慢遇到。
最后是 Lua 的注释,Lua 也分单行注释和多行注释,单行注释和 SQL 一样以 --
开头,多行注释则以 --[[
开头、]]
结尾,里面写注释。
-- 这是单行注释
--[[
这是多行注释
并且开头的 -- 和 [[ 之间不可以有空格,结尾是两个 ]
]]
--[[
这也是多行注释
不是两行单行注释
--]]
以上我们对 Lua 便有了一个初步认识,下面来学习 Lua 的数据结构。
Lua 的数值
Lua 的数值类型为 number,无论是整数还是浮点型,类型都是 number。
-- Lua 和 Python 类似,在创建变量时不需要指定类型,解释器会自动根据赋的值来判断
a = 123
b = 3.14
print(a, b) -- 123 3.14
-- Lua中,每一行语句的结尾也不需要加分号,直接换行即可
-- 当然加分号也可以,跟 Python 是类似的
c = 123;
d = 456
print(c, d) -- 123 456
-- 并且在 Python 中,如果加上了分号,那么两行赋值可以写一行
-- 比如 e = 1; f = 2,这在 Lua 中也是可以的
e = 1; f = 2
print(e, f) -- 1 2
-- 但 Lua 更加彪悍,不加分号也可以
-- 如果在 Python 中这么写,则肯定是报错的
g = 3 h = 4
print(g, h) -- 3 4
-- 但是我们不建议将多行赋值语句写在同一行里面,最好要分行写
-- 如果写在同一行,那么应该使用 Lua 的多元赋值
a, b = 1, 2
print(a, b) -- 1 2
可能有人发现了,我们在最上面已经创建 a 和 b 这两个变量了,但是最后又创建了一次,这一点和 Python 类似,可以创建多个同名变量。比如创建 a = 1,然后又创建 a = 2,这是允许的,只不过相当于发生了更新,将 a 的值由 1 变成了 2,当然即便赋值为其它类型也没问题。比如先创建 a = 数值,然后再将 a 的值换成字符串,这一点和 Python 一样,因为在 Lua 中,全局变量是通过 table、也就是"表"来存储的。
这个 table 后面会详细说,你暂时可以理解为哈希表,或者当成 Python 的字典,而且 Python 中全局变量也是通过字典存储的。
我们通过 Lua 的数值,演示了在 Lua 中如何创建一个变量,并且还介绍了 Lua 中全局变量的存储方式。然后是整数和浮点数的区分,既然它们的类型都是 number,那要怎么区分呢?
a = 123
b = 123. -- . 后面不写东西的话,默认是 .0
c = .123 -- . 前面不写东西的话,默认是 0.
print(a, b, c) -- 123 123.0 0.123
-- Lua 中,可以使用 type 函数检测变量的类型
print(type(a)) -- number
print(type(b)) -- number
print(type(c)) -- number
-- 这个 type 是内置的,它检测的是 lua 中的基础类型
-- 而我们说 Lua 不区分整型和浮点型,如果想精确区分的话,那么可以使用 math.type
-- 整型是 integer,浮点型是 float
print(math.type(a)) -- integer
print(math.type(b)) -- float
print(math.type(c)) -- float
-- 如果一个数值中出现了小数点,那么 math.type 得到的就是 float
使用 type 和 math.type 得到的都是一个字符串,另外我们是直接使用的 math.type,这个 math 类似于一个外部包。比如 Python 也有 math 包,只不过在 Lua 中不需要像 Python 那样显式导入,直接用即可,包括后面处理字符串用的包也是如此。
整数和浮点数之间的比较
整数和浮点数可以比较:
print(3 == 3.0) -- true
print(-3 == -3.0) -- true
-- 我们看到,如果小数点后面是 0,那么是相等的,这一点和 Python 一样
-- 另外 Lua 也支持科学计数法
print(3e3) -- 3000.0
-- Lua 中 a ^ b 表示 a 的 b 次方
-- 如果运算中出现了浮点数,或者发生了幂运算,那么结果就是浮点
print(3 ^ 2) -- 9.0
print(3 * 3) -- 9
-- Lua 也支持 16 进制
print(0x61) -- 97
算术运算
算数运算没啥好说的,如果是两个整数运算,那么结果还是整数,如果出现了浮点数,那么结果为浮点数。
print(1 + 2, 1 + 2.) -- 3 3.0
print(1 * 2, 1 * 2.) -- 2 2.0
print(1 - 2, 1 - 2.) -- -1 -1.0
print(13 % 5, 13. % 5) -- 3 3.0
但是除法例外,两个数相除的结果一定为浮点数。
print(3 / 2, 4 / 2, 4 / 2.) -- 1.5 2.0 2.0
Lua 还提供了地板除,会对商向下取整,这是在 Lua5.3 中引入的。
print(3 // 2, 4 // 2) -- 1 2
-- 另外,如果里面出现了浮点,那么即使是地板除,也一样会得到小数
print(4 // 2.) -- 2.0
-- 虽然是浮点,但结果是 1.0, 相当于还是有向下取整的效果
print(4 // 2.5) -- 1.0
当然 Lua 还有幂运算,使用 ^ 表示。
print(3 ^ 4) -- 81.0
print(3e4) -- 30000.0
只要出现了幂运算,得到的一定是浮点数。
位运算
数值之间还有关系运算,但比较简单,这里就不赘述了。只是需要注意:不等于在其它语言中都是 !=,而在 Lua 中是 ~=。
位运算和主流编程语言也是比较类似,尤其是 Python,感觉 Lua 的很多设计都和 Python 比较相似。
-- 按位与 &
print(15 & 20) -- 4
-- 按位或 |
print(15 | 20) -- 31
-- 按位异或 ~, 在其它语言中是 ^,在 Lua 中是 ~
print(15 ~ 20) -- 27
-- 取反, 取反的话也是 ~
print(~20) -- -21
-- 左移
print(2 << 3) -- 16
-- 右移
print(16 >> 2) -- 4
以上这些操作符是在 5.3 当中才提供的,如果是之前的版本,则不能使用这些操作符。
数学库
Lua 也提供了一个数学库,叫做 math,里面定义了一些用于计算的函数,比如:sin、cos、tan、asin、floor、ceil 等等。
这个在用的时候直接通过 IDE 提示,或者查询文档即可,这里就不演示了。
Lua 的字符串
下面我们看看 Lua 的字符串,字符串既可以使用双引号、也可以使用单引号。注意:Lua 的字符串是不可变量,不能本地修改,如果想修改只能创建新的字符串。
name = "komeiji satori"
print(name) -- komeiji satori
-- 使用 # 可以获取其长度
print(#name, #"name") -- 14 4
-- 使用 .. 可以将两个字符串连接起来
print("aa" .. "bb") -- aabb
print("name: " .. name) -- name: komeiji satori
-- .. 的两边可以没有空格,但为了规范,建议前后保留一个空格
-- 另外 .. 前后还可以跟数字,会将数字转成字符串
print("abc" .. 3, 3 .. 4, 3 .. "abc") -- abc3 34 3abc
-- 另外如果 .. 的前面是数字的话,那么 .. 的前面必须有空格
-- 也就是写成类似于 3 .. 的形式,不可以写 3..
-- 因为 3 后面如果直接出现了 . 那么这个 . 会被当成小数点来解释
另外如果 ..
的前面是数字的话,那么 ..
的前面必须有空格,也就是写成类似于 3 ..
的形式,不可以写 3..
。因为 3 后面如果直接出现了 .
,那么这个 .
会被当成小数点来解释。
Lua 内部也支持多行字符串,使用[[
和]]
表示。
msg = [[
你好呀
你在什么地方呀
你吃了吗
]]
print(msg)
--[[
你好呀
你在什么地方呀
你吃了吗
]]
字符串和数值的转换
Lua 中字符串可以和数值相加,也可以相互转换。
-- 如果字符串和整数运算,那么得到的是浮点数
-- 你可以认为只有整数和整数运算才有可能得到整数,而字符串不是整数
print("10" + 2) -- 12.0
print("10.1" + 2) -- 12.1
-- 调用 tonumber 函数可以将字符串显式地转为整数
print(type(tonumber("10"))) -- number
print(tonumber("10") + 2) -- 12
-- 如果转化失败,那么结果为 nil
print(tonumber("ff")) -- nil
-- 当然有些时候我们的数字未必是 10 进制,比如上面的 ff,它可以是 16 进制
-- 如果需要进制,那么就给 tonumber 多传递一个参数即可
print(tonumber("ff", 16)) -- 255
print(tonumber("11101", 2)) -- 29
print(tonumber("777", 8)) -- 511
-- 8 进制,允许出现的最大数是 7,所以转化失败,结果为 nil
print(tonumber("778", 8)) -- nil
-- 数值转成字符串,则是 tostring
print(tostring(100) == "100") -- true
print(tostring(100) == 100) -- false
print(tostring(3.14) == "3.14") -- true
所以数值和字符串是可以相加的,当然相减也可以,会将字符串转成浮点数。也可以判断是否相等或者不相等,这个时候会根据类型判断,不会隐式转化了,由于两者类型不一样,直接不相等。但两者无法比较大小,只能判断是否相等或者不等,因为 2 < 15 但 "2" > "15",所以为了避免混淆,在比较的时候 Lua 不会隐式转换、加上类型不同也无法比较大小,因此直接抛异常。
字符串标准库
Lua 处理字符串还可以使用一个叫 string 的标准库,这个标准库也是内嵌在解释器里面,我们直接通过 string.xxx 即可使用。下面就来看看 string 这个标准库都提供了哪些函数吧,补充一下 Lua 的字符串是以字节为单位的,不是以字符为单位的。因此 string 的大部分函数不适合处理中文(除了少数例外),如果要处理中文,可以使用后面介绍的 utf8。
-- 查看字符串的长度
print(string.len("abc"), #"abc") -- 3 3
-- 一个汉字占三个字节,默认是以字节为单位的,计算的是字节的个数
print(string.len("古明地觉"), #"古明地觉") -- 12 12
-- 重复字符串 n 次
print(string.rep("abc", 3)) -- abcabcabc
-- 如果是单纯的重复字符串的话,也可以对中文操作,因为不涉及字符的局部截取
print(string.rep("古明地觉", 3)) -- 古明地觉古明地觉古明地觉
-- 字符串变小写,可以用于中文,但是没意义
print(string.lower("aBc")) -- abc
print(string.lower("古明地觉")) -- 古明地觉
-- 同理还有转大写
print(string.upper("aBc")) -- ABC
print(string.upper("古明地觉")) -- 古明地觉
-- 字符串翻转,这个不适合中文
print(string.reverse("abc")) -- cba
print(string.reverse("古明地觉")) -- ��谜厘椏�
-- 我们看到中文出现了乱码,原因就是这个翻转是以字节为单位从后向前翻转
-- 而汉字占 3 个字节,需要以 3 个字节为单位翻转
-- 字符串截取,注意:Lua 中索引是从 1 开始的
-- 结尾也可以写成 -1,并且字符串截取包含首尾两端
print(string.sub("abcd", 1, -1)) -- abcd
print(string.sub("abcd", 2, -2)) -- bc
-- 可以只指定开头,不指定结尾,但是不可以开头结尾都不指定
print(string.sub("abcd", 2)) -- bcd
-- 同样不适合中文,除非你能准确计算字节的数量
print(string.sub("古明地觉", 1, 3)) -- 古
print(string.sub("古明地觉", 1, 4)) -- 古�
-- 超出范围,就为空字符串
print(string.sub("古明地觉", 100, 400) == "") -- true
-- 将数字转成字符
print(string.char(97)) -- a
-- 如果是多个数字,那么在转化成字符之后会自动拼接成字符串
print(string.char(97, 98, 99)) -- abc
-- 字符转成数字,默认只转换第 1 个
print(string.byte("abc")) -- 97
-- 可以手动指定转换第几个字符
print(string.byte("abc", 2)) -- 98
print(string.byte("abc", -1)) -- 99
-- 超出范围,那么返回 nil
print(string.byte("abc", 10) == nil) -- nil
-- 转换多个字符也是可以的,这里转化索引为 1 到 -1 之间的所有字符
print(string.byte("abc", 1, -1)) -- 97 98 99
-- 越界也没事,有几个就转化几个
print(string.byte("abc", 1, 10)) -- 97 98 99
-- 另外,这里是返回了多个值,我们也可以用多个变量去接收
a, b, c = string.byte("abc", 1, 10)
print(a, b, c) -- 97 98 99
-- 关乎 Lua 返回值,由于涉及到了函数,我们后面会说
-- 字符串的格式化,格式化的风格类似于 C
print(string.format("name = %s, age = %d, number = %03d", "古明地觉", 17, 1)) -- name = 古明地觉, age = 17, number = 001
-- 字符串的查找,会返回两个值,分别是开始位置和结束位置
print(string.find("abcdef", "de")) -- 4 5
-- 不存在则为 nil
print(string.find("abcdef", "xx")) -- nil
-- 字符串的全局替换,这个替换可以用中文,返回替换之后的字符串和替换的个数
print(string.gsub("古名地觉 名名 那么可爱", "名", "明")) -- 古明地觉 明明 那么可爱 3
-- 我们同样可以使用返回值去接
new_str, count = string.gsub("古名地觉 名名 那么可爱", "名", "明")
print(new_str) -- 古明地觉 明明 那么可爱
关于处理 ASCII 字符,string 库为我们提供了以上的支持,可以看到支持的东西还是比较少的,因为 Lua 的源码总共才两万多行,这就决定了它没办法提供过多的功能。Lua 主要是用来和别的语言结合的,并且 string 库提供的东西也不少了。
下面来看看 utf-8,我们说 string 库不是用来处理 unicode 字符的,如果处理 unicode 字符的话,需要使用 utf8 这个库。
-- Lua 中存储 unicode 字符使用的编码是 utf-8
-- 计算长度
print(utf8.len("古明地觉")) -- 4
-- 类似于 string.byte,这两个可以通用
print(utf8.codepoint("古明地觉", 1, -1)) -- 21476 26126 22320 35273
-- 类似于 string.char,这两个可以通用
print(utf8.char(21476, 26126, 22320, 35273)) -- 古明地觉
-- 截取,使用 string.sub,但不同字符占的字节大小可能不一样,这时候怎么截取呢
-- 可以通过 utf8.offset 计算出,偏移到第 n 个字符的字节量
print(string.sub("古明地觉", utf8.offset("古明地觉", 2))) -- 明地觉
print(string.sub("古明地觉", utf8.offset("古明地觉", -2))) -- 地觉
-- 遍历,遍历使用了 for 循环,我们后面说,现在先看一下
for i, c in utf8.codes("古a明b地c觉") do
print(i, c, utf8.char(c))
--[[
1 21476 古
4 97 a
5 26126 明
8 98 b
9 22320 地
12 99 c
13 35273 觉
]]
end
以上便是 Lua 处理字符串的一些操作, 尽管功能提供的不是非常的全面,但这与 Lua 的定位有关。
Lua 的控制结构
控制结构主要有两种:条件语句和循环语句。
条件语句
Lua 的条件语句和其它编程语言类似,我们举例说明。
-- 单独的 if
if condition then
statement
end
-- if 和 else
if condition then
statement
else
statement
end
-- if elseif else
-- 注意:是 elseif,不是 else if,else 和 if 之间需要连起来
if condition then
statement
elseif condition then
statement
elseif condition then
statement
else
statement
end
if 和 elseif 后面必须加上一个 then,类似于 Python 中必须加上一个冒号一样,但是 else 则不需要 then。另外每个 if 语句,在结尾处必须有一个 end,来标志这个 if 语句块的结束。不过既然结尾有 end,那么 Lua 中也就不需要缩进了,但 Python 则是必须严格遵循缩进规范,而 Lua 则不被缩进约束。但还是那句话,为了代码的可读性还是建议按照 Python 的规范来编写。
a = 85
if a > 80 then
print("a > 80")
end
-- a > 80
if a > 85 then
print("a > 85")
else
print("a <= 85")
end
-- a <= 85
if a < 60 then
print("不及格")
elseif a < 85 then
print("及格")
elseif a < 100 then
print("优秀")
elseif a == 100 then
print("满分")
else
print("无效的分数")
end
-- 优秀
既然 Lua 的 if 语句不受缩进影响,那么有时候单个 if 语句也可以写在一行。
a = 85
-- 这么写也是可以的
if a > 80 then print("a > 80") end
-- if 和 else 也可以写在一行
if a > 85 then print("a > 85") else print("a <= 85") end
-- 甚至 if elseif else 也可以写在一行,或者自由换行也是可以的
if a < 60 then
print("不及格") elseif a
< 85 then print("及格") elseif
a <= 100 then print("优秀") else
print("无效的分数") end
-- 对于 if elseif else,最好不要像上面那么写,尽管它是支持的,但不要这么做
-- 由此我们看到,Lua 不是靠缩进来规范语句的,而是靠关键字
当然嵌套 if 语句也是一样的。
age = 18
gender = "female"
if gender == "female" then
if age >= 18 then
print("成年女性")
else
print("未成年女性")
end
else
if age >= 18 then
print("成年男性")
else
print("未成年男性")
end
end
-- 成年女性
if gender == "female" then
if age >= 18 then
print("成年女性")
else print("未成年女性")
end
else
if age >= 18 then
print("成年男性")
else
print("未成年男性")
end end
-- 成年女性
所以我们不考虑缩进也是可以的,但是不要这么写,一定要按照规范来。
循环 while 语句
循环 while 语句,格式如下。
while condition do
statement
end
举个例子。
i =
1 sum = 0
while i < 10 do
sum = sum + i
i = i + 1
end
print(string.format("sum = %d", sum)) -- sum = 45
repeat ... until
repeat ... until 说白点就是一直重复做,直到满足某个条件停下来。
i =
1 sum = 0
-- 不断的执行 sum = sum + 1 和 i = i + 1,直到满足 i >= 10 的时候停下来
repeat
sum = sum + i
i = i + 1
until i >= 10
print(string.format("sum = %d", sum)) -- sum = 45
循环 for 语句
for 语句分为两种,我们来看一下。
-- 循环打印了 1 到 8
for a = 1, 8 do
print(a)
end
--[[
1
2
3
4
5
6
7
8
]]
-- 还可以跟上步长
for a = 1, 8, 3 do
print(a)
end
--[[
1
4
7
]]
上面是简单的遍历数字,for 循环还可以遍历表,等介绍表的时候我们再说。
break
break 用于跳出循环体,可以用于 for、while、repeat,注意:没有 continue。
for a = 1, 8 do
print(a)
break
end
-- 1
-- 如果 a 是偶数,那么我们就继续下一层循环
-- 但 Lua 中没有 continue,我们可以使用 if else 进行模拟
for a = 1, 8 do
if a % 2 == 0 then
-- 语句块里面可以不写任何东西
else
print(a)
end
end
--[[
1
3
5
7
]]
and 和 or
如果需要多个判断条件,那么可以使用 and 和 or 进行连接。
username = "satori"
password = "123456"
if username == "satori" and password == "123456" then
print("欢迎来到避难小屋")
else
print("用户名密码不对")
end
-- 欢迎来到避难小屋
-- 另外 Lua 中还有 not 表示取反,得到布尔值
-- 这里着重强调一点,在 Lua 中只有 false 和 nil 才为假,其它全部为真
-- 这里和 Python 不一样,在 Python 中 0、"" 是假,但在 Lua 中是真
print(not 0) -- false
print(not "") -- false
print(not not "") -- true
-- 0 和 "" 为真,所以使用 not 得到假,两个 not 得到真
以上我们就介绍了 Lua 的控制结构,比较简单。
Lua 的表
下面来看看 Lua 的表(Table),表是 Lua 语言中最主要(事实上也是唯一)的数据结构,表既可以当做数组来用,也可以当成哈希表来用。这个和 Python 的字典非常类似,比如我们之前查看变量类型的 math.type,本质上就是以字符串 "type" 来检索表 math。而在 Python 中,比如调用 math.sin,本质也是从 math 模块的属性字典里面查找 key 为 "sin" 对应的 value。
# python 代码
import math
print(math.sin(math.pi / 2)) # 1.0
print(math.__dict__["sin"](math.pi / 2)) # 1.0
然后看看在 Lua 中如何创建表。
-- 类似于 Python 的字典,Lua 中创建表直接使用大括号即可
t = {}
-- 返回的是表的一个引用
print(t) -- table: 00000000010b9160
-- 类型为 table
print(type(t) == "table") -- true
在这里我们需要介绍一下 Lua 的变量,在 Lua 中分为全局变量和局部变量,这两者我们会在函数中细说。总之目前创建的都是全局变量,其有一个特点:
-- 对于没有创建的变量,可以直接打印,结果是一个 nil
print(a) -- nil
-- c 这个变量没有创建,因此是 nil,那么 d 也是 nil
d = c
print(d) -- nil
-- 所以我们看到程序中明明没有这个变量,但是却可以使用,只不过结果为 nil
-- 那如果我们将一个已经存在的变量赋值为 nil,是不是等于没有创建这个变量呢?
-- 答案是正确的,如果将一个变量赋值为 nil,那么代表这个变量对应的内存就会被回收
name = "shiina mashiro"
name = nil -- "shiina mashiro" 这个字符串会被回收
之所以介绍全局变量这个特性,是因为在表中,nil 是一个大坑,我们往下看。
a = {}
a["name"] = "古明地觉"
a["age"] = 16
-- 打印 a 只是返回一个引用
print(a) -- table: 00000000000290e0
print(a["name"], a["age"]) -- 古明地觉 16
-- 更改表的元素
-- table 类似于哈希表,key 是不重复的,所以重复赋值相当于更新
a["age"] = a["age"] + 1
print(a["age"]) -- 17
-- 全局变量也是通过 table 存储的,我们可以给一个变量不断地赋值,赋上不同类型的值
a["age"] = 18
print(a["age"]) -- 18
a["age"] = "十六"
print(a["age"]) -- 十六
-- 创建 table 返回的是一个引用
b = a
-- 此时的 b 和 a 指向的是同一个 table,修改 b 会影响到 a
b["name"] = "satori"
print(a["name"]) -- satori
-- 赋值为 nil,等价于回收对象
a = nil
-- 但是只将 a 赋值为nil,显然还不够,因为还有 b 在指向上面的 table
b = nil
-- 这样的话,table 就被回收了
Lua 的 table 既可以做哈希表,也可以当做数组,有兴趣可以看 Lua 的源代码,非常的精简。下面来看看 table 如何当成数组来使用:
a = {}
for i = 1, 10 do
a[i] = i * 2
end
print(a[3]) -- 6
-- table 的底层是一个结构体,里面实现了哈希表和数组两种结构
-- 如果 key 是整型,那么会通过数组的方式来存储,如果不是,会使用哈希表来存储
-- 注意:如果当成数组使用,那么索引也是从 1 开始的
-- 此时是通过哈希表存储的
a["x"] = 233
print(a["x"]) -- 233
-- 除了a["x"]这种方式,还可以使用a.x,这两者在 Lua 中是等价的
print(a.x) -- 233
-- a["name"] 和 a.name 是等价的,但是和 a[name] 不是等价的
-- 因为 name 是一个变量,而 name = "x",所以结果是 a["x"] 或者 a.x
a["name"] = "椎名真白"
name = "x"
print(a["name"], a.name, a[name]) -- 椎名真白 椎名真白 233
然后是关于整数和浮点数的一个坑,来看一下。
a = {}
a[2] = 123
print(a[2.0]) -- 123
a[2.0] = 456
print(a[2]) -- 456
-- 所以这两者是等价的,因为 2.0 会被隐式转化为 2,事实上在 Python 的字典中也有类似的现象
-- d = {}; d[True] = 1; d[1] = 2; d[1.0] = 3; print(d)
-- 上面那行代码在 Python 里面执行一下,看看会发生什么
-- 但对于字符串则不一样,因为 2 和 "2" 不相等
a = {}
a[2] = 123
a["2"] = 456
print(a[2], a["2"]) -- 123 456
-- 如果访问表中一个不存在的 key 呢?
print(a["xxx"]) -- nil
-- 我们看到得到的是一个 nil
-- 显然我们想到了,如果将一个 key 对应的值显式地赋值为 nil,那么也等价于删除这个元素
a[2] = nil
表构造器
估计有人目前对 table 即可以当数组又可以当哈希表会感到困惑,别着急我们会慢慢说。目前创建表的时候,都是创建了一张空表,其实在创建的时候是可以指定元素的。
a = {"a", "b", "c" }
print(a[1], a[2], a[3]) -- a b c
-- 我们看到如果不指定 key,那么表的元素是通过数组存储的,这种存储方式叫做 "列表式(list-style)"
-- 索引默认是 1 2 3 4...
-- 此外,还可以这么创建
b = {name="mashiro", age=18 }
print(b["name"], b["age"]) -- mashiro 18
-- 第二种方式是通过哈希表存储的,这种存储方式叫做"记录式(record-style)"
-- 但如果存储的 key 是数字或者特殊字符,那么需要使用 [] 包起来
b = {["+"] = "add", [3] = "xxx"} -- 必须使用 ["+"] 和 [3],不能是单独的 + 和 3
-- 同理获取也只能是 b["+"] 和 b[3],不可以是 b.+ 和 b.3
print(b["+"], b[3]) -- add xxx
-- 表也是可以嵌套的
a["table"] = b
print(a["table"]["+"]) -- add
-- 此外,两种方式也可以混合使用
mix = {'a', name='mashiro', 'b', age=18 }
print(mix[1], mix[2]) -- a b
print(mix["name"], mix["age"]) -- mashiro 18
-- 这里有必要详细说明一下,即使是混合使用
-- 如果没有显式地指定 key、也就是列表式,那么会以数组的形式存储,索引默认是 1 2 3...
-- 所以 a[1] 是 'a', a[2] 是 'b'
-- 如果是这种情况呢?
mix = {'a', [2] = 1 }
print(mix[2]) -- 1
mix = {'a', 'b', [2] = 1 }
print(mix[2]) -- b
-- 解释一下,首先对于单个标量来说,默认就是用数组存储的,索引就是 1 2 3...
-- 但我们在通过记录式设置的时候,对应的 key 使用的如果也是数组的索引,那么记录式中设置的值会被顶掉
--[[
比如:mix = {'a', [2] = 1 }, 数组的最大索引是 1,所以 [2] = 1 是没有问题的
但是 mix = {'a', 'b', [2] = 1 },数组最大索引是 2,所以 [2] = 1 会被顶掉,因为冲突了
]]
-- 事实上 mix = {'a', 'b', [2] = 1 } 这种方式就等价于 mix = {[1] = 'a', [2] = 'b', [2] = 1 }
-- 如果 key 是整型,那么也通过数组存储, 否则通过哈希表存储
-- 只不过我们手动指定 [2] = 1 会先创建,然后被 [2] = 'b' 顶掉了
a = {'a', [1] = 1 }
print(a[1]) -- 'a'
a = {[1] = 1, 'a'}
print(a[1]) -- 'a'
-- 无论顺序如何,a[1] 都会是 'a'
估计有人还有疑问,那就是a = {}; a[1] = 1; a[100] = 100
或者a = {1, [100] = 100}
,如果这样创建的话,那中间的元素是什么?因为我们说 key 是整型则以数组存储,而数组又是连续的存储的空间,而我们只创建了两个元素,索引分别是 1 和 100,那么其它元素是以什么形式存在呢?带着这些疑问,我们先往下看。
数组、列表和序列
现在我们知道了如果想表示常见的数组、或者列表,那么只需要使用整型作为索引即可。而且在 Lua 的 table 中,可以使用任意数字作为索引,只不过默认是从 1 开始的,Lua 中很多其它机制也遵循此惯例。
但是table的长度怎么算呢?我们知道对字符串可以使用 #,同理对 table 也是如此。
a = {1, 2, 3, name = 'mashiro', 'a' }
print(#a) -- 4
-- 但是我们看到结果为 4,可明明里面有 5 个元素啊
-- 因为 # 计算的是索引为整型的元素的个数,更准确的说 # 计算的是使用数组存储的元素的个数
a = {[0] = 1, 2, 3, 4, [-1]=5}
print(#a) -- 3
-- 此时的结果是 3,因为 0 和 -1 虽然是整型,但它们并没有存储在数组里
-- 因为 Lua 索引默认从 1 开始,如果想要被存储的数组里面,那么索引必须大于 0
a = {1, 2, [3.0]="xxx", [4.1] = "aaa" }
print(#a) -- 3
-- 这里同样是 3,因为 3.0 会被隐式转化为 3,因此数组里面有 3 个元素,但是 4.1 不会
所以我们看到,# 计算的是存储在数组里面的元素,也就是 table 中索引为正整数的元素,但真的是这样吗?
首先对于数组中存在nil的 table,使用 # 获取长度是不可靠的,它只适用于数组中所有元素都不为 nil 的 table。事实上,将 # 应用于获取 table 长度一直饱受争议,以前很多人建议如果数组中存在 nil,那么使用 # 操作符直接抛出异常,或者说扩展一下 # 的语义。然而这些建议都是说起来容易做起来难,主要是在 Lua 中数组实际上是一个 table,而 table 的长度不是很好理解。
我们举例说明:
a = {1, 2, 3, 4 }
a[2] = nil
-- 很容易得出这是一个长度为 4,第二个元素为 nil 的 table
print(#a) -- 4
-- 但是下面这个例子呢?没错,就是我们之前说的
b = {}
b[1] = 1
b[100] = 100
-- 是否应该认为这是一个具有 100 个元素,其中 98 个元素为 nil 的 table 呢?
-- 如果我们再将 a[100] 设置成 nil,该列表长度又是多少呢?是 100、99 还是 1 呢
print(#b) -- 1
-- Lua 作者的想法是,像 C 语言使用 \0 作为字符串的结束一样,Lua 可以使用 nil 来隐式地表示 table 的结束
-- 可问题是 a 的第二个元素也是 nil 啊,为什么长度是 4 呢
-- 总之在 table 中出现了 nil,那么 # 的结果是不可控的
-- 有可能你多加一个 nil,结果就变了。当然,不要去探究它的规律,因为这没有意义
-- 总之不要在 table 中写 nil,在 table 中写 nil 是原罪。不管是列表式、还是记录式,都不要写 nil,因为设置为 nil,就表示删除这个元素
-- 回到 b 这个 table 中,我们说它的长度为 1
print(#b) -- 1
-- 但是数组中确实存在索引为 100 的元素
print(b[100]) -- 100
所以对 b 这个 table,其中数组到底是怎么存储的,其实没必要纠结,就当成索引为 2 到索引为 99 的元素全部是 nil 即可,但计算长度的时候是不准的,总之 table 中最好不要出现 nil。
遍历表
我们可以使用 for 循环去遍历 table。
a = {"a", "b", name="mashiro", "c", age=18, "d" }
-- for 循环除了 for i = start, end, step 这种方式之外,还可以作用在表上面
-- 只不过需要使用 pairs 将 table 包起来:for k, v in pairs(t)
for index, value in pairs(a) do
print(index, value)
--[[
1 a
2 b
3 c
4 d
age 18
name mashiro
]]
end
-- 这里的 for 循环中出现了两个循环变量,分别表示索引和值
-- 如果只有一个变量,那么得到的是索引,或者哈希表的 key
-- 然后遍历的时候先遍历数组(按照索引从小到大输出),然后遍历哈希表(不保证顺序)
-- 除了 pairs,还有 ipairs,ipars 是只遍历存在于数组里面的元素
a = {[4] = "a", [3] = "b", name="mashiro", "c", age=18, [2] = "d" }
for index, value in ipairs(a) do
print(index, value)
--[[
1 c
2 d
3 b
4 a
]]
end
-- 打印按照索引从小到大打印,但是不建议这么创建table
如果 table 中出现了 nil,那么使用 for 循环去遍历会发生什么奇特的现象呢?
-- 不过在此之前,还是先来看一个坑向的
a = {[3] = 1, 'a', 'b', 'c' }
-- 这个时候 a[3] 是多少呢?
print(a[3]) -- c
-- 我们说只要是列表式,都是从 1 开始,所以 [3] = 1 最终会被 [3] = 'c' 所顶掉
-- 上面的赋值等价于 a = {[3] = 1, [1] = 'a', [2] = 'b', [3] = 'c'}
-- 因为如果不指定 key,那么 Lua 会按照 1 2 3 4 ··· 自动给一个 key(准确来说是索引),因为它们存在数组中
-- 再来看看 table 中出现了 nil,for 循环会如何表现
a = {'a', nil, 'b', 'c' }
print(#a) -- 4
for index, value in ipairs(a) do
print(index, value)
--[[
1 a
]]
end
-- 长度虽然是 4(当然我们知道这不准),但在遍历的时候一旦遇到 nil 就会终止遍历
-- 当然这个 nil 要是数组中的 nil,不是哈希表中的 nil
-- 但如果是 pairs,那么会遍历值不为 nil 的所有记录
a = {'a', nil, 'b', 'c', name=nil, age=18}
for index, value in pairs(a) do
print(index, value)
--[[
1 a
3 b
4 c
age 18
]]
end
-- 但我们看到值 "b" 对应的索引是 3,尽管前面的是 nil,但毕竟占了一个坑,所以 "b" 对应的索引是 3
-- 当然我们还可以使用获取长度、数值遍历的方式,当然前提是 table 中不能出现 nil
a = {'a', 'b', 123, 'xx' }
for idx = 1, #a do
print(a[idx])
--[[
a
b
123
xx
]]
end
表标准库
表的标准库提供一些函数,用于对表进行操作,注意:这个标准库也叫 table。
a = {10, 20, 30 }
print(a[1], a[2], a[3]) -- 10 20 30
-- 使用 table.insert 可以插入一个值
-- 接收参数为:table 插入位置 插入的值
table.insert(a, 2, "xxx")
print(a[1], a[2], a[3], a[4]) -- 10 xxx 20 30
-- 如果不指定位置,那么默认会添加在结尾
-- 此时传递两个参数即可:table 插入的值
table.insert(a, "古明地觉")
print(a[#a]) -- 古明地觉
-- 既然有 insert,那么就会有 remove
-- 接收参数:table 移除的元素的位置(索引)
print(a[1], a[2], a[3], a[4], a[5]) -- 10 xxx 20 30
table.remove(a, 3)
print(a[1], a[2], a[3], a[4], a[5]) -- 10 xxx 30 古明地觉 nil
-- 我们看到使用 remove 之后,后面的元素会依次向前移动,因此无需担心会出现 nil 什么的
-- 不过这也说明了,remove 的效率不是很高,因为涉及到元素的移动
-- 但 table 中的函数都是 C 实现的,也是很快的,因此也不用太担心
-- 另外在 lua5.3 中,还提供了一个 move 函数
-- table.move(table, start, end, target),表示将 table 中 [start, end] 之间的元素移动到索引为 target 的位置上
-- 也是 start 位置的元素跑到 target 处,start + 1 -> target + 1、 end -> target + end - start
t = {1, 2, 3, 4}
table.move(t, 2, #t, 3)
print(t[1], t[2], t[3], t[4], t[5]) -- 1 2 2 3 4
-- 很好理解,{1 2 3 4} 中索引为 [2, #t],移动到索引为 3 的位置上,因此结果是1 2 2 3 4,结果会多出一个
-- 这里的 move 实际上是将一个值从一个地方拷贝 copy 到另一个地方
-- 另外,我们除了可以将元素移动到 table 本身之外,还可以移动到另一个 table
t1 = {"a", "b", "c", "d" }
t2 = {"x", "y" }
-- 表示将 t1 中 [2, #t1] 的元素移动到 t2 中索引为 2 的地方
table.move(t1, 2, #t1, 2, t2)
for idx = 1, #t2 do
print(t2[idx])
--[[
x
b
c
d
]]
end
-- table 标准库中还提供了 concat 函数,会将表里面的元素拼接起来
a = {1, 2, "xxx", 3, "aaa" }
print(table.concat(a)) -- 12xxx3aaa
来个思考题吧
a = "b"
b = "a"
t = {a = "b", [a] = b }
print(t.a, t[a], t[t.b], t[t[b]])
-- 上面的 print 会打印出什么呢?我们分析一下,首先看 t 这个表,其中 a = "b" 无需多说
-- 关键是 [a] = b,我们说 a 和 b 都是变量,并且 a = "b"、b = "a", 所以结果等价于 ["b"] = "a", 即:b = "a"
-- 因此这里的 t 可以看做是 {a = "b", b = "a"}
-- 那么 t.a 显然是 "b",t[a]等于t["b"],因此结果是 "a"
-- t.b 结果是 "a",那么 t[t.b] 等于是 t["a"],所以结果是 "b"
-- t[b] -> t["a"] -> "b",那么 t[t[b]] -> t["b"] -> "a",因此结果是 "a"
-- 所以 print 会打印出: "b" "a" "b" "a"
-- 下个问题
a = {}
a.a = a
print(a) -- table: 0000000000d98ef0
print(a.a) -- table: 0000000000d98ef0
print(a.a.a) -- table: 0000000000d98ef0
-- 打印的都是一样的,我们说 Lua 中的 table 返回的一个引用
-- a.a = a,本身显然陷入了套娃的状态
以上就是 Lua 的表,总的来说并不复杂,只是要注意里面不要出现 nil 就好。然后 table 采用了两种数据结构:数组和哈希表,它即可以当成数组来用,也可以当成哈希表来用,当然也可以混合使用。如果 key 是整数,那么存在数组中,否则存在哈希表中。
Lua 的函数
下面我们来介绍一下 Lua 的函数,Lua 的函数可以说非常的有意思,尤其是它的参数和返回值的设定很有趣。不过在介绍之前,我们需要先说一下 Lua 的全局变量和局部变量。
-- 直接创建的变量,默认是全局的,在哪里都可以使用
-- 如果想创建一个局部变量,那么需要使用 local 关键字,这样创建的变量就只在对应的作用域中生效
if 2 > 1 then a = 123 end
print(a) -- 123
-- 当上面的 if 语句执行完之后,a 这个全局变量就被创建了
if 2 > 1 then local b = 123 end
print(b) -- nil
-- 此时打印的是 nil,因为上面 if 语句中的变量 b 在创建时使用了 local 关键字
-- 代表它是一个局部的,只能在对应的 if 语句中使用,外面没有 b 这个变量,所以打印结果为 nil
name = "mashiro"
if 2 > 1 then local name = "satori" end
print(name) -- mashiro
if 2 > 1 then name = "satori" end
print(name) -- satori
-- 如果是 local,那么相当于创建了新的局部变量,if 里面的 name 和外部的 name 是不同的 name
-- 但如果没有 local,那么创建的都是全局变量,因此相当于直接对外部的 name 进行修改
for i = 1, 10 do
end
-- 不仅是 if,for 循环也是如此,里面如果使用了 local 关键字创建变量,那么外部也是无法使用的
-- 这里我们看一下循环变量 i, 变量 i 在循环结束之后也不能使用了
print(i) -- nil
i = 0
for i = 1, 10 do
end
print(i) -- 0
-- 我们看到打印的是 0,说明 for 循环的 i 和外部的 i 是没有关系的
-- 不仅是 for 循环,while 循环和 repeat until 也是如此
i = 1 sum = 0
repeat
sum = sum + i
-- 尽管 x 是局部变量,但是它对 until 是可见的
local x = sum
until x > 30
-- 再比如函数,还没有介绍,但是可以先感受一下
function add()
b = "aaa"
end
print(b) -- nil
add()
print(b) -- aaa
-- 当我们直接 print(b) 结果为 nil,但是 add() 之后 b = "aaa" 就执行了
-- 而我们说,只要没有 local,那么创建的变量都是全局的,所以再次打印 b 就得到了字符串 "aaa"
-- 另外如果是在全局的话,即便加上了 local,它还是一个全局变量
a = "xx"
local a = "xx"
-- 上面两种方式没有区别,因为这是在全局中定义的,所以即使加上了 local 也没问题
然后我们来看看如何在 Lua 中定义一个函数,Lua 函数的定义规则如下:
function func_name(arg1, arg2) do
statement
statement
statement
...
end
Lua 函数的参数传递
我们来看看给函数传递参数该怎么做?
function f1(a, b, c)
print(a, b, c)
end
f1(1, 2, 3) -- 1 2 3
f1(1, 2) -- 1 2 nil
f1(1, 2, 3, 4) -- 1 2 3
-- 我们看到如果参数传递的不够,会自动使用 nil 填充
-- 如果传递多了,会自动丢弃。另外 lua 函数不支持关键字参数传递
-- Lua 函数也不支持默认参数,但是通过上面这个特性,我们可以实现默认参数的效果
function f2(a, b, c)
-- 我们希望给 c 一个默认参数,假设就叫 "xxx" 吧
-- 如果 c 为 nil,那么结果就是 "xxx",因为 Lua 中 false 和 nil 为假
c = c or "xxx"
print(a, b, c)
end
f2(1, 2, 3) -- 1 2 3
f2(1, 2) -- 1 2 xxx
Lua 函数的返回值
Lua 函数支持多返回值。
-- 比如之前使用的 string.find 函数,也是返回了两个值
function f1()
-- 使用 return 返回,如果没有 return,那么相当于返回了一个 nil
end
x = f1()
print(x) -- nil
function f2()
return 1, 2
end
-- 接收的变量和返回值一一对应,所以 x 是返回值的第一个值
x = f2()
print(x) -- 1
x, y, z = f2()
print(x, y, z) -- 1 2 nil
-- 如果接收的变量多于返回值的个数,那么剩下的变量使用 nil 填充
然后看一下 Lua 函数返回值的一些特殊情况。
function f1()
return "a", "b"
end
x, y = f1()
print(x, y) -- a b
x = f1()
print(x) -- a
x, y, z = f1()
print(x, y, z) -- a b nil
-- 上面的都很好理解,这个也简单
x, y, z = 10, f1()
print(x, y, z) -- 10 a b
-- 那么,下面的结果如何呢?
x, y, z = 10, f1(), 11
print(x, y, z) -- 10 a 11
-- 我们看到只用了 f1 返回的一个值
x, y, z = f1(), 10
print(x, y, z) -- a 10 nil
-- 惊了,难道不应该是 a b 10 吗
Lua的返回值有如下规律:
如果等号的右边只有一个函数调用,比如 x, y, z = f1(),那么 f1 所有的返回值都会被使用,分别按照顺序分配给 x、y、z 三个变量,不够的赋为 nil,多余的丢弃
如果等号的右边除了函数调用,还有其它的值,比如:x, y, z = f1(), 10,那么如果调用不是在最后一个,则只返回一个值,如果在最后一个,那么会尽可能返回多个值
怎么理解呢?举个例子。
- x, y, z = f1(), 10,显然 f1() 的右边还有值,那么不好意思,不管 f1 返回多少个值,只有第一个有效。
- x, y, z = 10, f1(),由于 f1() 的右边没有值了,显然它是最后一个,那么要尽可能返回多个值,10 给了 x,那么 f1 返回的 "a" 和 "b" 就会给 y 和 z。
- x, y, z = 10, 20, f1(),这个时候 10 和 20 会赋值给 x 和 y,那么尽管 f1 返回两个值,但是只剩下一个变量了,所以 f1 的第一个返回值会赋值给 z。
再次举例说明。
function f1()
return "a", "b"
end
-- f1() 后面没有东西了,位于最后一个,因此尽可能匹配更多的返回值
x, y, z = 10, f1()
print(x, y, z) -- 10 a b
-- f1 返回两个值,加上后面的 10 正好三个,看似能够匹配 x y z
-- 但 f1() 是一个函数调用,它的后面还有东西,因此在这种情况下,我们说 f1 只有一个返回值生效
-- 如果 f1 没有返回值,那么相当于返回了一个 nil
-- 所以按照顺序匹配的话,x = f1的第一个返回值,y = 10, z = nil
x, y, z = f1(), 10
print(x, y, z) -- a 10 nil
-- 尽管 f1() 在最后面,但我们说是尽可能多的匹配
-- x 和 y 已经找到 10 和 20 了,所以只能是 f1 的第一个返回值赋值给 z
x, y, z = 10, 20, f1()
print(x, y, z) -- 10 20 a
-- 显然此时已经轮不到 f1 了
x, y, z = 10, 20, 30, f1()
print(x, y, z) -- 10 20 30
function f2() end
-- 即使 f2 什么也没有返回,但是会给一个 nil
x, y, z = f2(), 10
-- 所以 x 是 nil,y 是 10,z 是 nil
print(x, y, z) -- nil 10 nil
相信此时你对 Lua 函数的返回值已经有一个大致的了解了,但上面的例子中,函数调用的右边只是一个普通的值,如果是多个函数调用怎么办?我们来看看。
function f1()
return 1, 2
end
function f2()
return 3
end
x, y, z = f1(), f2()
print(x, y, z) -- 1 3 nil
-- 我们看到结果和之前是类似的
-- f1() 后面还有东西,尽管不是普通的值,但不管咋样,有东西就对了
-- f1() 不是最后一个,那么不好意思,只有返回值的第一个会赋值给变量
-- 因此 1 会赋值给 x,f2() 位于最后一个,会尽可能多的匹配,但是只有一个值
-- 因此 f2 返回的 3,会赋值给 y,而 z 的话就是 nil
x, y, z = f1(), f2(), "xx"
print(x, y, z) -- 1 3 xx
-- 如果 f2 返回了两个值呢?
function f2() return 3, 4 end
x, y, z = f1(), f2()
print(x, y, z) -- 1 3 4
-- 很好理解
x, y, z = f1(), f2(), "xx"
print(x, y, z) -- 1 3 xx
-- f1()和 f2() 后面都有东西,因此都只有返回的第一个值生效
到此 Lua 函数的返回值我们已经揭开它的庐山真面目了,但函数的返回值还有一个特点, 我们来看一下。
function f1()
return "a", "b"
end
-- 这没有问题
print(f1()) -- a b
-- 我们看到函数依旧无法摆脱这个命运
-- 即便是打印,如果后面还有东西,那么只有自身的第一个返回值会被打印出来
print(f1(), 1) -- a 1
-- 对于其它的函数也是如此
print(string.find("hello world", "wor")) -- 7 9
print(string.find("hello world", "wor"), "xxx") -- 7 xxx
-- 事实上不光是 print,比如函数的返回值可以作为另一个函数的参数
function f1() return 1, 2 end
function f2(a, b, c) print(a, b, c) end
-- 我们看到,除了赋值,作为另一个函数的参数,也是如此
f2(f1()) -- 1 2 nil
f2("xx", f1()) -- xx 1 2
f2(f1(), "xx") -- 1 xx nil
-- 同理,即便是对于表,也是一样的
t = {f1() }
-- 很好理解,元素个数为 2,就是 f1 的返回值
print(#t, t[1], t[2]) -- 2 a b
t = {f1(), "xxx" }
print(#t, t[1], t[2]) -- 2 a xxx
-- 惊了,明明多加进去一个元素,居然还只有两个元素
-- 说明即使在表中,只要函数调用后面有东西,函数的返回值只有第一个生效
-- 最后 Lua 函数还有一个特点
-- 如果将函数调用,再次使用括号括起来,那么强制只有第一个返回值返回
a, b = f1()
print(a, b) -- a b
a, b = (f1())
print(a, b) -- a nil
-- 当使用 () 将函数调用包起来之后
-- 使得无论函数返回了多少个值,其结果只有第一个值有效
-- 因此对于 a, b = (f1()) 来说,a 的结果就是 f1 函数的第一个返回值,b 为 nil
当然这些特性我们不用太在意,因为我们使用 Lua 一般都是用来编写扩展,只要保证代码规范,这些坑基本不会遇见。
Lua 函数的可变长参数
Lua 函数可以通过可变长参数来接收任意个数的参数,通过 ... 来实现。这个 ... 就是 Lua 函数可变长参数的定义方式,我们使用的时候直接使用这个 ... 即可。
-- 可变长参数 ... 一定要位于普通的参数之后
function f1(a, b, ...)
-- 刚才我们举的例子中,定义的变量都是全局变量
-- 但是工作中,函数里面的变量如果不需要外界使用,那么应该定义成局部的
-- 当然即便外界需要使用,也可以通过返回值的方式。只不过为了方便,所以就没有加 local 关键字
-- 我们来看看这个 ... 是什么玩意
print(..., type(...))
end
-- 首先 1 会传递给 a,2 会传递给 b,剩余的参数会都传递给 ...
-- 我们看到它是一个number
f1(1, 2, 3, "xxx") -- 3 number
f1(1, 2, "xxx", 3) -- xxx string
-- 我们似乎看到了一个奇特的现象,明明给 ... 传递了两个参数
-- 但是从结果上来看,貌似相当于只传递了一个,再来举个栗子
function f2(...)
local a, b = ...
print(a, b)
end
f2("a") -- a nil
f2("a", "b") -- a b
f2("a", "b", "c") -- a b
我们看到 ... 确实不止一个参数,具体是几个则取决于调用时传递了几个,但是直接打印的时候只有第一个生效,查看类型的时候也是查看第一个值的类型。所以这个 ... 算是比较奇葩的东西,为什么说奇葩呢?因为它没有特定的类型,这个 ... 只能在函数中使用,至于到底是什么类型,则取决于第一个值的类型,但它又不止一个值。
-- 首先 print(nil) 是可以打印出来东西的,结果就是 nil
print(nil) -- nil
-- 一个不存在的变量结果和 nil 相等
print(a == nil) -- true
function f1(...)
print(... == nil)
end
-- 我们看到当什么都不传的时候,结果等于 nil
f1() -- true
-- 但如果我们尝试打印这个 ... 的时候,是打印不出来 nil 的
function f2(...)
print(type(...))
end
f2(123) -- number
-- 代码报错
f2()
--[[
C:\lua\lua54.exe: lua/main.lua:16: bad argument #1 to 'type' (value expected)
stack traceback:
[C]: in function 'type'
lua/main.lua:16: in function 'f2'
lua/main.lua:19: in main chunk
[C]: in ?
]]
我们看到执行 f2() 的时候报错了,提示我们:type 函数需要一个 value,但我们明明传递了一个 ... 过去啊。原因是 ... 在接收不到值的时候,那么它就相当于不存在一样,虽然在和别的值进行比较的时候、或者说赋值的时候,... 会是 nil,但是在作为函数的参数的时候,则相当于不存在。比如 print(...) 你以为会打印出 nil 吗?答案是不会的,此时的 print(...) 等价于 print();同理 type(...) 等价于 type(),而 type 需要一个参数,所以报错了。
所以这个 ... 算是比较奇葩的一个存在,我们可以换个方式来理解,尽管这样肯定是不准确的,但却可以从某种角度上变得容易理解。
function f1(...)
local a, b = ...
print(a, b, 2 == ..., ...)
end
-- 假设我们执行 f1(2, 3, 4) 的时候,2,3,4 会传递给 ...。对于赋值来说,你可以认为把 ... 替换成了 2,3,4
-- 因此 local a, b = ... 等价于 local a, b = 2, 3, 4,所以 a 是 2、b 是 3
-- 但是对于比较、或者作为函数参数来说,可以认为是把 ... 换成了 2,3,4 中的第一个值
f1(2, 3, 4) -- 2 3 true 2 3 4
-- 2 == ... 为什么是 true 呢?因为 ... 的第一个值是 2
-- 如果我们什么也不传的话,假设是执行 f1(),显然没有参数会传递给 ...
-- 因此此时的 ... 就什么也不是,你也可以认为这个 ... 不存在
-- 如果是赋值或者比较的话,那么 ... 会变成 nil,如果是作为参数则等于不存在
-- 因此 local a, b = ... 等价于 local a, b = nil
-- 2 == ... 等价于 2 == nil, 至于 print(...) 则等于 print()
f1() -- nil nil false
-- 所以执行 f1() 的时候,print 只打印了3个值,因为 ... 相当于不存在
-- 当然我们也可以显式的传递 nil
function f2(...)
local a, b, c, d = ...
print(a, b, c, d)
end
f2(nil, 1, nil, 2) -- nil 1 nil 2
-- 即便是 nil,也会按照顺序原封不动地传递过去
但有些时候,我们不知道 ... 究竟代表了多少个参数,这个时候怎么办呢?答案是变成一个 table。
function f1(...)
local t = {... }
print(table.concat(t))
end
-- 此时的 t = {...} 等价于 t = {1, 2, "xxx"}
f1(1, 2, "xxx") -- 12xxx
-- 如果出现了 nil,比如:执行 f1(1, 2, nil, "xxx") 会报错,因为 nil 不能被合并
function f2(...)
local t = {... }
for idx, val in pairs(t) do
print(idx, val)
end
end
f2(1, 2, nil, "xxx")
--[[
1 1
2 2
4 xxx
]]
-- 我们看到里面的 nil 并没有被打印出来,因为 pairs 会打印所有值不为 nil 的
-- 如果是 ipairs,那么 "xxx" 也不会被打印,因为 ipairs 遍历到 nil 就结束了
所以遍历 ... 的时候,可以将其放在 {} 里面变成一个表,但缺陷是里面会出现 nil,尽管在遍历的时候可以使用 pairs 保留所有不为 nil 的值,但还是不够完美,我们希望能够将所有的值保留下来。这个时候可以使用 table.pack,将 ... 变成一个表,这种做法和 {...} 的区别就是,前者保留了所有的值,并且还提供了一个额外的值计算表中元素的个数。下面举例说明:
function f1(...)
-- 会返回一个表,这个表和我们平时创建的表虽然都是表,但是调用 table.pack 返回的表会有一个额外的属性:n
-- 执行 t.n 会返回表中所有元素个数,也包括 nil
-- 需要注意的是:我们平时手动创建的表没有 n 这个属性,只有调用 table.pack 返回的表才有这个属性
local t = table.pack(...)
-- 获取表的元素个数,我们之前使用的是 #t
-- 对于调用 table.pack 返回的表也可以这么做,只是结果未必使我们想要的
print(t.n, #t)
end
f1(nil, nil, 3, 4, 5) -- 5 5
f1(1, 2, 3, nil, nil) -- 5 3
f1(nil, nil, 3, 4, nil) -- 5 4
-- 我们看到无论什么时候,t.n 返回的永远是表中元素的个数
-- 但是 #t 就不一定了,从上面的结果可以看到实在让人有点捉摸不透
-- 所以说不要在创建表的时候在里面写 nil,这是原罪,因为写上了 nil,你不知道元素到底有多少个
-- 但是有些对技术非常专研的人,可能会探究过 nil 出现的个数、以及出现的位置,对 #t 产生的影响
-- 甚至发现了一些规律,并且感觉还满靠谱的。但是千万不要认为这是对的,也没有必要去探究
-- 总之不要在表中写 nil,但是对于当前这个例子而言,因为表示通过对 ... 打包得到的
-- 而 ... 是认为手动传递的,数量显然是在我们的掌控范围内,而 t.n 也能准确返回元素个数,所以在 ... 中传递 nil 是没有问题的
-- 我们也可以进行遍历
function f2(...)
local t = table.pack(...)
for i = 1, t.n do
print(t[i])
end
end
f2(nil, 1, 2, nil, 3)
--[[
nil
1
2
nil
3
]]
-- 所有的值都被打印的出来
另一种遍历可变长参数的方式是使用 select 函数。
-- select(n, "a", "b", "c", ...) 的作用是返回第 n 个元素以及之后的所有元素
-- 如果 n 为 "#",那么会返回所有元素的个数
print(select(1, "a", "b", "c")) -- a b c
print(select(2, "a", "b", "c")) -- b c
print(select(3, "a", "b", "c")) -- c
print(select(4, "a", "b", "c")) --
print(select("#", "a", "b", "c")) -- 3
function f1(...)
print(select("#", ...))
end
-- select 获取元素也是返回包括 nil 在内的所有元素的个数
print(f1(nil, nil, 3, 4, nil)) -- 5
-- 比如我们可以算出传入的参数中,值为整数的和
function sum(...)
local s = 0
for i = 1, select("#", ...) do
local val = select(i, ...)
if type(val) == "number" then
-- 这里我们进行了检测,如果能确保传递的一定是number
-- 那么也可以直接写成 s = s + select(i, ...)
-- 因为运算的话,也是只有返回的第一个值会参与运算
s = s + val
end
end
return s
end
print(sum(1, 2, 3, 4)) -- 10
print(sum(1, 2, 3, "xx", 4)) -- 10
print(sum(1, 2, nil, "xx", 4)) -- 7
另外 table.pack,可以将 ... 打包成一个表,同理 table.unpack 也可以对一张表进行解包。
function f1(a, b)
print(a, b)
end
t = {1, 2, 3 }
f1(table.unpack(t)) -- 1 2
print(
string.find("mashiro", "shiro")
) -- 3 7
print(
string.find(
table.unpack({"mashiro", "shiro"})
)
) -- 3 7
-- 所以传递参数的时候,f1(a, b) <==> f1(table.unpack({a, b}))
--[[
如果熟悉 Python 的话,那么这个类似于 Python 中的 *
def f1(a, b):
pass
f1(1, 2) <==> f1(*[1, 2])
]]
table.unpack 还可以只对部分元素进行解包。
function f1(a, b)
print(a, b)
end
t = {1, 2, 3, 4, 5 }
-- 对于索引为 2 以及后面的元素进行解包,所以结果是 2 3 4 5,因此 a 和 b 为 2、3
f1(table.unpack(t, 2)) -- 2 3
f1(table.unpack(t, 4)) -- 4 5
f1(table.unpack(t, 5)) -- 5 nil
f1(table.unpack(t, 6)) -- nil nil
-- 对索引为 1 到 5 的元素进行解包
f1(table.unpack(t, 1, 5)) -- 1 2
-- 反着写则不存在
f1(table.unpack(t, 3, 1)) -- nil nil
-- 起始和结束相等,那么结果只有一个值
f1(table.unpack(t, 4, 4)) -- 4 nil
Lua 函数的 goto
之所以把 goto 放在最后面,是因为它不是必须存在的,有的人甚至不建议使用 goto,但是在某些时候 goto 还是很有用的,所以还是要介绍一下它。事实上这个 goto,肯定不用我说,大家都知道它是用来跳转的。
需要注意的是:Lua 的 goto 只能在函数中使用,当然其它语言也是如此,并且跳转只能在当前的代码块中进行跳转,一个函数不可能通过 goto 跳转到另一个函数里面去。
function f1(a)
-- 定义一个标签,通过 ::标签名:: 来进行定义
-- 之所以设置成这样,也是为了给程序员一个提示作用吧
::label1::
print(11)
end
f1() -- 11
-- 标签定义之后,代码依旧正常执行
-- 没有 goto,你可以认为标签相当于不存在
function f2()
-- 跳转到 label1 中
goto label1
::label1::
print(123)
::label2::
print(456)
end
f2()
--[[
123
456
]]
-- 注意:跳转到一个标签的时候,会执行其后面的所有代码
-- 标签只是一个位置:跳转到这个位置,然后执行该位置后面的所有代码
function f3()
-- 跳转到label2中
goto label2
::label1::
print(123)
::label2::
print(456)
end
f3() -- 456
通过标签,我们可以使用 continue 的功能。
function f1()
for i = 1, 5 do
if i == 3 then
goto continue
end
print(i)
-- 直接跳转到结尾即可
::continue::
end
end
f1()
--[[
1
2
4
5
]]
以上就是 Lua 的函数,虽然 Lua 很简单,但是注意事项还真不少。不过绝大部分情况,我们都不需要太在意,只要将这些坑避免掉即可,比如表中不出现 nil,函数的形参和调用时传递的实参个数相匹配等等。按照其它语言中的规范去写,那么基本不会出现错误。
Lua 的输入、输出
由于 Lua 语言强调可移植性和嵌入性,所以 Lua 本身并没有提供太多与外部交互的机制,从图形、数据、网络访问等等大多数 IO 操作,应该由宿主程序来完成。如果不考虑一些外部库,但就 Lua 本身来说,只能操作一些基本的文件。
Lua 提供了一个库叫做 io,专门用来进行操作文件的。当然 print 函数也是通过 io 来实现的,因为 print 进行打印,本质上是将内容写入到控制台当中,而控制台你也可以理解为一个文件。
io.write("xxx")
io.write("yyy")
-- xxxyyy
-- print 也是调用了 io.write,但是我们看到最终输出了 xxxyyy
-- 因为 print 会自动增加一个换行,而 io.write 是将内容原本输出
至于读取也很简单,使用 io.read() 即可从命令行中读取一行。
-- 执行完之后会卡主
name = io.read()
-- 输入mashiro,然后回车,那么程序会读取输入,赋值给name,执行下面代码
print(name) -- mashiro
age = io.read()
-- 从控制台当中读取的都是 string 类型
print(age, type(age)) -- 16 string
读取和写入文件
io.read 和 io.write 可以用来从控制台输入、输出,如果是文件的话,则是 io.open(文件名, 模式)。
f = io.open("1.txt", "r")
--[[
关于文件的读取模式,有以下几种
r:只读,文本模式,文件必须存在
w:只写,文本模式,不存在会创建,存在则清空
a:追加,文本模式,不存在会创建,存在则追加
rb:只读,二进制模式,文件必须存在
wb:只写,二进制模式,不存在会创建,存在则清空
ab:追加,二进制模式,不存在会创建,存在则追加
r+:可读可写,文件同样要求必须存在,指针会自动位于文件的开头位置
w+:可读可写,文件不存在会创建,存在则清空
a+:可读可写,文件不存在会创建,存在则追加,指针位于文件的结尾
rb+、wb+、ab+ 也是同理
]]
print(f) -- file (00007ffff819fa90)
-- 我们看到读取文件得到的是一个 userdata 类型
print(type(f)) -- userdata
-- f 是一个文件句柄,如果想要读取里面的内容,需要使用 f:read,注意不是 f.read
-- 关于 : 运算符,我们会在后面讨论,目前只需要知道通过 : 来调用read即可
print(f:read("a"))
--[[
when i was young,
i'd listen to the radio,
waiting for my favourite song
]]
-- 但是我们看到在 read 函数里面传入了一个字符串 "a"
-- 如果不传,那么只会读取一行,传入字符串"a",那么会全部读取进来
-- 一旦文件读取完毕,指针会移到文件的结尾,那么再次读取会返回 nil
print(f:read()) -- nil
io.read 里面如果不传值,那么会只读取一行,传入 "a" 则是全部读取,除此之外还可以传入一个数字,读取指定字节的文本。
f = io.open("1.txt", "r")
-- 读取一行,这里不包括换行符,如果想把换行符也读取进来的话,可以通过 f:read("L")
-- 而事实上,f:read() 等价于 f:read("l"),表示读取一行的同时不读取结尾换行符
print(f:read()) -- when i was young,
-- 再次读取 40 个字符
print(f:read(40))
--[[
i'd listen to the radio,
waiting for my
]]
-- 关闭文件
f:close()
至于写文件我们就不说了,比较简单,总之记得关闭文件,然后内容会写到文件里面,当然程序结束时也会关闭文件。但如果想在程序运行期间强行将内容刷到文件里面去,可以使用 f:flush(),和其它编程语言都是类似的。
如果读取的时候文件不存在,怎么办?
f, err_msg, err_code = io.open("不存在的文件.txt", "r")
print(f) -- nil
print(err_msg) -- 不存在的文件.txt: No such file or directory
print(err_code) -- 2
-- 如果文件不存在,会返回三个值
-- 分别是 nil、错误信息、以及错误码
-- 关于状态码需要解释一下,文件读取实际上调用的是操作系统的接口,文件不存在操作系统会返回一个错误
-- 对于每个错误类型,都有一个状态码,这个状态码所有编程语言都是一样的,因为它们调用的是操作系统的接口
-- 以 Python 为例,文件不存在会抛出一个 OSError
--[[
try:
open("xxx")
except OSError as e:
print(e.errno) # 2
print(e) # [Errno 2] No such file or directory: 'xxx'
print(e.args) # (2, 'No such file or directory')
]]
-- 状态码都是一样的
-- 另外,我们还可以使用 assert 函数
f = assert(io.open("不存在的文件", "r"))
--[[
C:\lua\lua54.exe: lua/main.lua:27: 不存在的文件: No such file or directory
stack traceback:
[C]: in function 'assert'
lua/main.lua:27: in main chunk
[C]: in ?
]]
-- 这里就报错了,如果文件不存在,那么错误信息会被展示出来。并且这里是发生了异常,程序在此就终止了
-- 但我们有时候并不知道文件是否存在,所以希望在文件不存在的时候,程序还能往下走
-- 这个时候就不要使用 assert,而是读取之后,检测是否为 nil
io 库还提供了三个预定义的三个 C 语言流的句柄:io.stdin、io.stdout、io.stderr。
-- 从控制台中读取除了 io.read(),还可以使用 io.stdin:read()
name = io.stdin:read()
-- 写入控制台,除了 io.write(name),还可以使用 io.stdout:write(name)
io.stdout:write(name, '\n') -- mashiro
-- 除了写入的话,除了 io.stdout 还有 io.stderr,这两者都会将内容输出到控制台
-- 但 io.stderr 表示标准错误文件,io.stdout 表示标准输出文件
io.stderr:write(name, '\n') -- mashiro
-- io.stderr:write 打印出来的内容会变成红色,表示输出的一个错误信息
-- 另外,尽管我们是先调用的 io.stdout:write,后调用的 io.stderr:write
-- 但是当打印的时候,你会发现红色的 "mashiro" 在上面,说明 io.stderr:write 写入的内容先被打印了出来
-- 因为 stdout 是行缓冲,stderr 不带缓冲区,会直接输出,目的是为了更早地看到错误信息
io 还提供了 input 和 output,io.read 实际上就是 io.input():read 的缩写、io.write 是 io.output():write 的缩写。
name = io.input():read()
io.output():write(name) -- mashiro
其它文件操作
文件的指针是可以移动的。
f = io.open("1.txt", "r")
-- 我们读取了 15 个字节,所以指针会偏移 15 个字节
f:read(15)
-- 调用 f:seek() 即可获取指针当前的位置
print(f:seek()) -- 15
-- 当然我们还可以移动指针,移动指针有三种方式:
-- f:seek("set", count) 从文件开头的位置向后移动 count 个字节, 返回移动文件指针的位置
-- f:seek("cur", count) 从指针当前的位置向后移动 count 个字节, 返回移动文件指针的位置
-- f:seek("end", count) 从文件结尾的位置向后移动 count 个字节, 返回移动文件指针的位置
-- 如果 count 小于 0,那么就是向前移动 abs(count) 个字节
-- 从开头向后移动 11 个字节,返回 11,当然也可以单独使用 f:seek() 进行获取
print(f:seek("set", 11)) -- 11
-- 从当前(11)位置向后移动 11 个字节,结果是 11 + 11
print(f:seek("cur", 11)) -- 22
-- 从结尾位置向前移动 11 个字节,事实上即便指针超出文件字节数量范围也没有错,只不过读取到的是一个 nil
print(f:seek("end", -11)) -- 63
-- 如果不传递第二个参数、也就是偏移量的话,那么相当于传递了个 0
print(f:seek("set", 0)) -- 0
print(f:seek("cur", 0)) -- 0
-- 事实上,我们可以通过这种办法来计算文件的字节数
-- 直接将指针移动到文件结尾,返回的结果就是文件的字节数
print(f:seek("end", 0)) -- 74
io 还提供一个 io.tmpfile 会返回一个操作临时文件的句柄,该句柄是以读写模式打开的,当程序运行结束后,该临时文件会被自动删除。
f = io.tmpfile()
-- 由于是可读写,那么写完之后文件指针会处于结尾
f:write("xxxx")
-- 此时如果想读到内容,需要将文件指针移到开头
f:seek("set", 0)
print(f:read("a")) -- xxxx
此外除了 io,还有一个 os。os.rename 可以对文件重命名,os.remove 可以删除文件,不过大部分情况下我们都用不到。并且这些函数在报错时,都会返回 nil、错误信息、错误码。
其它系统调用
函数 os.exit 可以用来终止程序的执行,里面可以传递两个参数,都是可选的。第一个参数为程序返回的状态,可以是一个数值 0 或者布尔 true,表示执行成功;第二个参数如果为 true,那么会关闭 Lua 状态并调用所有析构器释放占用的内存(不过这种方式通常是非必要的,因为操作系统会在进程退出时释放其占用的资源)。
-- os.getenv 可以用来获取环境变量
print(os.getenv("USERNAME")) -- satori
-- os.execute 则是执行系统命令,类似于 Python 中的 os.system,并且函数有三个返回值
-- 第一个返回值是布尔型,为 true 表示成功运行完成
-- 第二个返回值是一个字符串,如果字符串是 "exit",表示正常结束,为 "signal" 表示因信号而中断
-- 第三个返回值是返回状态(前提是正常终结)或者终结该程序的信号代码
-- 下面举例说明
a, b, c = os.execute("cd ..")
print(a) -- true
print(b) -- exit
print(c) -- exit
执行操作系统命令,还有一个 io.popen。对于 os.execute 来说,执行的命令结果会显示在控制台,但 io.popen 则是将内容输入到指定位置。
f = io.popen("python 2.py", "r")
print(f:read()) -- C:\Users\satori
Lua 的闭包
在 Lua 语言中,函数是严格遵循词法定界(lexical scoping)的第一类值(first-class value)。第一类值意味着 Lua 语言中的函数与其它常见类型的值(例如数值和字符串)具有同等权限:一个程序可以将某个函数保存到变量、或表中,也可以将函数作为参数传递给另外一个函数,还可以将函数作为某个函数的返回值返回。
而词法定界意味着,内层函数可以访问外层函数中的变量。这两个特性组合起来,为 Lua 语言带来了极大的灵活性。
函数是第一类值
我们来看看 Lua 是如何体现函数是第一类值的。
a = print
a("hello world") -- hello world
-- 函数可以作为一个变量自由传递
function f(func, str, substr)
print(func(str, substr))
end
f(string.find, "mashiro", "shi") -- 3 5
function f(func, str, start, end_)
print(func(str, start, end_))
end
f(string.sub, "satori", 2, -2) -- ator
-- 函数可以作为参数传递给另一个函数
function f()
return string.gsub
end
-- 函数也可以返回一个函数
print(f()("hello mashiro", "mashiro", "satori")) -- hello satori 1
-- 对于表也是一样的
t = {print, std = io.write }
t[1]("mashiro") -- mashiro
t["std"]("satori\n") -- satori
-- 两个函数也能替换
math.type = string.sub
print(math.type("xxx", 2, -2)) -- x
标准库 table 提供了一个 sort 函数,专门用来对表里面的内容进行排序,但是怎么排,Lua 没有提供具体的方法,而是需要我们通过一个函数来指定。
t = {
{name = "mashiro", age = 16},
{name = "satori", age = 17},
{name = "nagisa", age = 20},
{name = "kurisu", age = 18},
}
-- 会对元素进行比较,因为 a.age > b.age 的话,结果为 true
-- 所以年龄大的,会排在前面
table.sort(t, function(a, b) return a.age > b.age end)
print(t[1].name, t[1].age) -- nagisa 20
像这种没有函数名的函数,我们称之为匿名函数。匿名函数不能单独定义,需要作为值进行传递,比如放到表中、作为参数、或者使用变量进行接收等等。
非全局函数
由于函数是第一类值,因此一个显而易见的结果就是: 函数不仅可以被存储在全局变量中,还可以存储在表字段和局部变量中。
lib = {}
function add(a, b)
return a + b
end
lib.a = add
print(lib.a(1, 22)) -- 23
上面是将函数存储在表中,我们已经见到了,下面看看将函数存储在局部变量中是什么样子的。
function outer(a, b)
function inner(c)
return a + b + c
end
-- 这里必须要返回 inner,否则是无法调用 inner的
return inner
end
-- 上面我们就实现了一个闭包
print(outer(1, 2)(33)) -- 36
-- 但是问题来了,我们看到 inner 居然可以直接调用,因为我们说不管在什么地方,只要没有 local 关键字,定义的都是全局的
-- 所以尽管不能上来就调用 inner,但是当 outer 调用之后,inner 就会被加入到全局变量中
-- 可问题是,结果为啥是 6,这是因为 inner 里面需要 a 和 b,所以当 outer 调用之后,inner 就记住了 a 和 b 的值
-- 而我们上面给 outer 的 a 和 b 传递的值是 1 和 2,因此再调用 inner 的时候,a 和 b 还是 1 和 2
print(inner(3)) -- 6
print(inner(8)) -- 11
-- 重新执行 outer,那么 inner 会被重新定义,此时 inner 记住的 a 和 b 就变成了 11 和 22
print(outer(11, 22)(33)) -- 66
print(inner(1)) -- 34
那么如何将函数定义成局部的呢?
function outer(a, b)
local function inner(c)
return a + b + c
end
return inner
end
-- 此时就将 inner 定义成局部的了,因为我们使用了 local 关键字,所以 local 也可以作用于函数
print(outer(1, 2)(3)) -- 6
print(inner) -- nil
-- 那如何将函数保存起来呢
function f1(a)
local f2 = function(b) return a + b end
return f2
end
print(f1(1)(22)) -- 23
print(f2) -- nil
将函数保存在局部变量中有一个坑,我们来看看。
function f1()
local function f2(n)
if n == 1 then
return 1
else
return n + f2(n - 1)
end
end
return f2
end
-- 上面是一个简单的求和,通过递归来实现
print(f1()(100)) -- 5050
-- 如果是下面这种形式呢
function f1()
local f2 = function(n)
if n == 1 then
return 1
else
return n + f2(n - 1)
end
end
return f2
end
-- 我们代码和上面的版本没有太大区别,只不过将函数使用局部变量保存了
-- 执行的时候会出现什么结果呢?
f1()(100)
上面递归求和的第二个版本是否存在问题呢?答案是存在的,当调用内部的 n + f2(n - 1) 的时候,你会发现 f2 不存在。为什么是这个结果也很好理解,我们用右边那一大坨函数赋值给 f2,但是在赋值给 f2 的同时,里面就已经引用了 f2。所以此时的 f2 是一个 nil,或者说会去外部去找这个 f2。
function f1()
local f2
f2 = function(n)
if n == 1 then
return 1
else
return n + f2(n - 1)
end
end
return f2
end
-- 我们代码和上面的版本没有太大区别,只不过将函数使用局部变量保存了
-- 执行的时候会出现什么结果呢?
print(f1()(100)) -- 5050
此时就没有问题了,因为我们先定义了一个局部变量 f2,尽管定义的时候 f2 还没有正确的值,但是一旦当执行函数的时候,f2 就已经有了正确的值。
词法定界
关于词法定界,其实我们上面已经见识到了, 就是内层函数访问外层函数的变量。
a = "xxx"
function f1()
local a = 123
local function f2()
return a
end
return f2
end
-- 此时返回的是 f1 中的 a,内层函数可以访问外层函数的变量
print(f1()()) -- 123
a = "xxx"
function f1()
a = 123
local function f2()
return a
end
return f2
end
-- f1 中的 a 和全局的 a 是同一个 a
print(f1()(), a) -- 123 123
a = "xxx"
function f1()
local a = 123
local function f2()
local a = ">>>"
return a
end
return f2
end
-- 自身有 a 这个变量,就不会到外界去找了
print(f1()()) -- >>>
再举个栗子:
function f1(str)
local function f2(substr)
return string.find(str, substr)
end
return f2
end
f2 = f1("abcde")
print(f2("ab")) -- 1 2
print(f2("bcd")) -- 2 4
print(f2("abcde")) -- 1 5
以上就是 Lua 的闭包。
Lua 的日期和时间
Lua 有两个函数,用于操作日期和时间,我们来看一下。
-- 调用 os.time 会返回当前时间的时间戳
print(os.time()) -- 1592124891
-- 也可以指定年月日时分秒
print(os.time({year=2020, month=5, day=3, hour=14, min=33, sec=51})) -- 1588487631
-- 还有 os.date
-- 自动打印当前的日期,除此之外还可以传入一个时间戳,来打印指定格式的日期
-- 返回的是一个 string 类型
print(os.date("%Y-%m-%d")) -- 2020-06-14
print(os.date("%Y-%m-%d", os.time({year=2020, month=4, day=3}))) -- 2020-04-03
Lua 中关于日期的符号如下:
编译、执行和错误
虽然我们把 Lua 语言称为解释型语言( Interpreted Language ),但 Lua 语言总是在运行代码前先预编译( precompile )源码为中间代码(这没什么大不了的,很多解释型语言也这样做)。编译( compilation )阶段的存在听上去超出了解释型语言的范畴,但解释型语言的区分并不在于源码是否被编译,而在于是否有能力(且轻易地)执行动态生成的代码。
下面我们会详细学习 Lua 语言运行代码的过程、编译究竟是什么意思和做了什么、 Lua 语言是如何运行编译后代码的、以及在编译过程中如何处理错误。
编译
Lua 提供了一个 load 函数,我们来看看是做什么的。
f = load("i = 1 + 1")
-- 当调用 f 的时候,load 里面的字符串就会当成代码来执行
-- 有点类似于 Python 的 exec
print(i) -- nil
f()
print(i) -- 2
尽管 load 的功能很强大,但还是要谨慎使用,因为相比其他的可选函数而言,load 的开销比较大。另外我们当前将 load 函数的返回值保存了下来,但是也可以不保存。
-- 直接调用
load("s = '这是字符串'")()
print(s) -- 这是字符串
-- load 里面还可以放入一个字符串形式的函数
-- 多行的话,我们可以通过[[ ]]来代替引号
load(
[[
function f()
return "hello mashiro"
end
]]
)()
print(f()) -- hello mashiro
如果 load 中的字符串不符合 Lua 的语法规则,那么会得到什么呢?
print(load("s = '这是字符串'")) -- function: 00000000001bd010
-- 我们看到如果符合语法规则,那么会返回一个函数
-- 不符合的话,会返回 nil 和错误信息
print(load("s === '这是字符串'")) -- nil [string "s === '这是字符串'"]:1: syntax error near '=='
-- 因此最好的办法是使用 assert 函数,如果里面的值为 nil 或者 false,直接把异常抛出来
-- 比如 assert(2 == 2) 正常通过,assert(2!=2) 就会报错,因为里面的值为 false
-- 而如果为 false 或者 nil,那么 assert 就会抛出异常
-- 如果是返回了多个值,那么就看第一个值是否为 nil 或者 false
print(load("s = '这是字符串'")) -- function: 00000000001bd010
assert(load("s === '这是字符串'"))
-- 返回值的第一个为 nil,所以报错了,会把异常值抛出来
--[[
C:\lua\lua54.exe: lua/main.lua:9: [string "s === '这是字符串'"]:1: syntax error near '=='
stack traceback:
[C]: in function 'assert'
lua/main.lua:9: in main chunk
[C]: in ?
]]
关于 assert,可能有人不是很清楚,这里来说一下。
-- assert是用于断言的
assert(2 == 2)
print("正常通过")
-- 如果是 assert(2 != 2),就通不过了
function f1()
return 123, nil
end
assert(f1())
-- 上述 f1 函数返回了两个值,而第一个值为真,所以正常通过
function f2()
return nil, 123
end
-- 这里就通不过了,因为 f2 的返回值的第一个为 nil
-- 如果报错了,那么会将返回值的第二个作为异常信息
assert(f2())
--[[
C:\lua\lua54.exe: 123
stack traceback:
[C]: in function 'assert'
lua/main.lua:23: in main chunk
[C]: in ?
]]
-- assert 一旦断言失败,那么程序就终止了,不会再往下走了
回到 load 函数,load 函数里面如果是字符串,其实没有什么意义。f = load("i = 1") 和 function f() i = 1 end 两者是完全等价的。但由于后者、也就是定义函数的方式,代码会与其外层的函数一起被编译,所以其执行速度要快得多。与之对比,调用函数 load 时会进行一次独立编译。不过由于涉及到词法定界,上述示例的两段代码不一定完全等价。
i = 123
function f1()
local i = 0
f = load("i = i + 1; print(i)")
g = function () i = i + 1;print(i) end
f()
g()
end
f1()
--[[
124
1
]]
print(i) -- 124
-- 我们看到全局中有一个 i,函数 f1 中有一个局部变量 i
-- 对于 load 来说,会直接去找全局变量 i,因为 load 总是在全局环境中编译代码段
-- 而内层函数在找 i 的时候,会现在自身中找、然后是外层函数、最后是全局变量
-- 所以 f() 执行打印 124,此时外部的 i 也被修改了,内层函数由于找的是局部变量 i,所以打印的是 1
函数 load 最典型的用法是执行外部代码,比如我们可以在控制台中输入代码,因为收到的是字符串,所以正好可以通过 load 将其当成代码来执行。但是这样能处理的内容有限,所以在 Lua 中还提供了一个 loadfile,可以将一个文件里面的字符串当成代码来执行。
function add(a, b)
return a + b
end
a = 123
b = "xxx"
上面的文件名为 1.txt,我们通过 loadfile 进行加载,加载的时候不需要文件名一定以 .lua 结尾,只是文件里的内容符合 Lua 的语法规范即可。
loadfile("1.txt")()
print(add(11, 22)) -- 33
print(a) -- 123
print(b) -- xxx
错误
"人非圣贤孰能无过",因此我们必须尽可能地处理错误。并且由于 Lua 是一种经常被嵌入到应用程序中的扩展语言,所以当错误发生时不能简单地崩溃或者退出。相反只要错误发生,Lua 必须提供处理错误的方式。
Lua 语言在遇到非预期情况时会引发错误,例如将两个非数值类型的值相加,对不是函数的值进行调用,对不是表类型的值进行索引等等。我们也可以显示地调用函数 error 并传入一个错误信息作为参数来引发一个错误,通常这个函数就是在代码中提示出错的合理方式。
error("程序崩溃了")
--[[
C:\lua\lua54.exe: lua/main.lua:1: 程序崩溃了
stack traceback:
[C]: in function 'error'
lua/main.lua:1: in main chunk
[C]: in ?
]]
由于针对某些情况调用函数 error 太常见了,所以 Lua 还提供了内置函数 assert 来完成这类工作。assert 不需要多说,会检测第一个参数是否为真,如果为真,那么正常通过;否则报错,同时会将第二个参数作为异常值。
assert 总是在执行之前先对参数求值。
assert(nil, "hello" .. " " .. "satori")
--[[
C:\lua\lua54.exe: lua/main.lua:1: hello satori
stack traceback:
[C]: in function 'assert'
lua/main.lua:1: in main chunk
[C]: in ?
]]
这里是先将后面的字符串拼接起来之后,再执行 assert,事实上这算是废话,哪个函数在执行前不是先确定参数的。但是问题来了,我们的目的是希望在异常发生时,能够让其沉默,说人话就是异常捕获。而在 Lua 中实现异常捕获的话,需要使用函数 pcall,假设要执行一段 Lua 代码并捕获里面出现的所有错误,那么需要首先将这段代码封装到一个函数中,然后通过 pcall 来调用这个函数。
function f()
a = "xxx"
return 1, 2, 3
end
print(pcall(f)) -- true 1 2 3
print(a) -- xxx
-- pcall 里面需要传入一个函数,然后 pcall 来调用
-- 如果正确执行,那么 pcall 的返回值为:true 加上 pcall里面的函数的返回值
-- 如果执行失败,那么返回值为:false 加上 错误信息
-- 也可以直接传入一个匿名函数
print(pcall(function() print("" > 1) end)) -- false lua/main.lua:15: attempt to compare number with string
-- 如果报错,我们可以手动写入错误信息,通过函数error实现
status, err = pcall(function() error({code=666, err_msg="出错啦"}) end)
print(status, err.code, err.err_msg) -- false 666 出错啦
Lua 的异常捕获显然没有传统的 try catch 方便,但是对于一个小巧的脚本语言也足够了。
错误信息和栈回溯
-- 我们看到 pcall 里面传入的是一个函数名
-- 这就要求函数不能有参数,或者说 pcall 在调用的时候不会传递参数
-- 那么函数参数就全部为 nil,因此如果函数需要参数该怎么办?
function f(arg)
if type(arg) ~= "string" then
error("需要一个字符串")
end
end
-- 答案是把调用放在一个函数里
pcall(function() f("xxx") end)
-- 另外,如果我们在这里传递的不是一个字符串的话
print(pcall(function()f(123) end)) -- false lua/main.lua:6: 需要一个字符串
-- 告诉我们需要一个字符串,但是报错提示在第 6 行,也就是函数 f 中
-- 但这明显不是 f 的问题,而是我们在匿名函数中值传递错误了
function f(arg)
if type(arg) ~= "string" then
-- 可以指定一个层级,告诉 error 实际发生错误的地方是第 2 层
-- 第一层为 f 本身
error("需要一个字符串", 2)
end
end
print(pcall(function()f(123) end)) -- false lua/main.lua:25: 需要一个字符串
-- 我们看到报错信息变成了 25 行,也就是上面这一行
未完待续。
如果觉得文章对您有所帮助,可以请囊中羞涩的作者喝杯柠檬水,万分感谢,愿每一个来到这里的人都生活愉快,幸福美满。
微信赞赏
支付宝赞赏