八、Lua脚本详解 —— 超详细操作演示!
- 八、Lua脚本详解
- 8.1 Lua 简介
- 8.2 Linux 系统的Lua
- 8.2.1 Lua 下载
- 8.2.2 Lua 安装
- 8.2.3 Hello World
- 8.3 Win 系统的Lua
- 8.4 Lua 脚本基础
- 8.4.1 注释
- 8.4.2 数据类型
- 8.4.3 标识符
- 8.4.4 运算符
- 8.4.5 函数
- 8.4.6 流程控制语句
- 8.4.7 循环控制语句
- 8.5 Lua 语法进阶
- 8.5.1 table
- 8.5.2 迭代器
- 8.5.3 模块
- 8.5.4 元表和元方法
- 8.5.5 面向对象
- 8.5.6 协同线程与协同函数
- 8.5.7 文件IO
- 九、分布式锁
- 9.1 分布式锁的工作原理
- 9.2 问题引入
- 9.3 setnx 实现方式
- 9.4 为锁添加过期时间
- 9.5 为锁添加标识
- 9.6 添加 Lua 脚本
- 9.7 Redisson 可重入锁
- 9.8 Redisson 红锁
- 9.9 分段锁
- 9.10 Redisson 详解
数据库系列文章:
关系型数据库:
- MySQL —— 基础语法大全
- MySQL —— 进阶
非关系型数据库:
- 一、Redis 的安装与配置
- 二、Redis 基本命令(上)
- 三、Redis 基本命令(下)
- 四、Redis 持久化
- 五、Redis 主从集群
- 六、Redis 分布式系统
- 七、Redis 缓存
八、Lua脚本详解
8.1 Lua 简介
Lua 是一个由标准 C 语言 开发的、开源的、可扩展的、轻量级的、弱类型的、解释型脚本语言, 是 于 1993 年由 巴西里约热内卢天主教大学的三人研究小组使用标准 C 语言开发。
Lua 的官网 为: https://www.lua.org/
Lua 是一门 脚本语言,和 Shell、Python 是同一种类型。
用的最多的是 Unity 手游,做 热更新 方案;Nginx 也有应用。
8.2 Linux 系统的Lua
8.2.1 Lua 下载
若要使用 Lua 则需要先从官网下载其源码并安装。
8.2.2 Lua 安装
先将下载好的 Lua 源码上传到 Linux ,然后再进行安装。
⭐️(1)解压
将Lua 源码解压到 /opt/apps 目录。
tar -zxvf lua-5.4.6.tar.gz -C /opt/apps/
进入到 /opt/apps
下的 lua 目录可以看到编译用的 Makefile
文件 及 源码目录 src
。
⭐️(2)安装gcc
由于 Lua 是由 C/C++ 语言编写的,所以对其进行 编译 就必须要使用相关编译器。对于 C/C++ 语言的编译器,使用最多的是 gcc
。
yum -y install gcc gcc-c++
⭐️(3)编译
执行编译命令 make linux test
。
# test 测试输出版本号
make linux test
⭐️(4)安装
make install
安装完毕后,可以通过 lua -v
查看版本号,与前面 make linux test
中最后显示的结果是相同的。
如果
lua -v
显示的还是老版本,reboot
重启一下 Linux 系统就好了。
8.2.3 Hello World
⭐️(1)两种交互模式
Lua 为用户提供了两种交互模式:命令行模式 与 脚本文件模式。
A、命令行模式
该模式是,直接在命令行中输入语句,回车即可看到运行结果。
在任意目录下使用 lua
命令进入 lua 命令行模式
,在其中输入语句后回车即可运行显示出结果。使用 Ctrl
+ C
退出模式。
需要注意, lua 对语句后的 分号要求 不是 强制性的,有没有都行。
B、脚本文件模式
该模式是先要编写脚本文件,然后再使用 lua 命令
来运行文件。
例如直接创建一个名称为 hello.lua
的文件,文件中就写一名 print()
语句即可。
然后 直接运行“ lua 脚本文件
” 即可看到结果。
lua hello.lua
⭐️(2)两种脚本运行方式
对于脚本文件的运行有两种方式。
- 一种是上面的
lua 命令
方式, - 还有一种是
可执行文件
方式。可执行文件方式是,将 lua 脚本文件直接修改为 可执行文件 运行。
下面就使用第二种方式来运行。
A、修改脚本文件内容
在脚本文件第一行增加 #!/usr/bin/lua
,表示当前文件将使用 /usr/bin/lua
命令来运行。
#!/usr/bin/lua
B、修改脚本文件权限
chmod 755 hello.lua
为脚本文件赋予 可执行权限。
C、运行
直接使用文件名即可运行。
8.3 Win 系统的Lua
这里要安装的是在 Windows 系统中 Lua 的运行环境。最常用的为 SciTE 。
SciTE 是一款 Lua 脚本 测试编辑器,提供 Lua 的编辑运行环境。其官方下载地址为: https://github.com/rjpcomputing/luaforwindows/releases 。 下载完直接运行 exe
文件安装。
SciTE 提供了两种运行方式:命令行窗口运行方式 与 Lua 脚本的编辑运行环境。
除了SciTE ,还有像 LuaDist 、 LuaRocks 等。
8.4 Lua 脚本基础
8.4.1 注释
Lua 的 行注释 为两个连续的减号, 段注释 以--[[
开头,以 --]]
结尾。
不过,在 调试过程 中如果想 临时取消
段注释,而直接将其标识删除,这样做其实并不好。因为有可能还需要再添加上。而段注释的写法相对较麻烦。
- 所以, Lua 给出了一种简单处理方式:在开头的
--[[
前再加一个减号,即可使段注释不起作用。其实就是使两个段注释标识变为了两个行注释。
--~ 行注释,(快捷键为 Ctr + Q)
-- 行注释,(波浪号 ~ ,为快捷键自动生成的)--[[段注释
print("Hello, Lua")
--]]---[[取消段注释
print("Hello, Lua")
--]]
8.4.2 数据类型
Lua 中有 8 种 类型,分别为: nil
、 boolean
、 number
、 string
、 userdata
、 function
、 thread
和 table
。
- 通过
type()
函数可以查看一个数据的类型,例如,type(nil)
的结果为nil
,type(123)
的结果为number
。
-- string 演示
str1 = "中国"
str2 = '北京'
str3 = [[深圳
广州
上海]]print(str1)
print(str2)
print(str3)--[[输出:
中国
北京
深圳
广州
上海
--]]
8.4.3 标识符
程序设计语言中的标识符主要包含 保留字、变量、常量、方法名、函数名、类名 等。Lua 的标识符由 字母、数字 与 下划线 组成,但 不能以数字开头 。 Lua 是 大小写敏感的。
⭐️(1)保留字
Lua 常见的保留字共有 22
个。不过,除了这 22 个外, Lua 中还定义了很多的 内置全局变量 ,这些内置全局变量的一个共同特征是,以下划线开头后跟全大写字母 。所以我们在定义自己的标识符时不能与这些保留字、内置全局变量重复。
⭐️(2)变量
Lua 是弱类型语言,变量 无需类型声明 即 可直接使用。变量分为 全局变量 与 局部变量。Lua 中的变量 默认都是全局变量,即使声明在语句块或函数里。全局变量一旦声明,在当前文件中的(声明后)任何地方 都可访问。局部变量 local
相当于 Java 中的 private
变量,只能在声明的语句块中使用。
-- 局部变量
local x = 3-- 定义一个函数
function f()-- 全局变量y = 5-- 再定义一个局部变量local z = 8-- 访问局部变量print("x = "..x);
end-- 访问函数
f(); -- 输出 x = 3
-- 访问全局变量
print("y = "..y) -- 输出 y = 5
-- 访问局部变量
print("z = "..z) -- 报错,z 为局部变量
⭐️(3)动态类型
Lua 是 动态类型语言,变量的类型 可以随时改变,无需声明。
y = 5
print("y = "..y) -- 输出 y = 5
y = "北京"
print("y = "..y) -- 输出 y = 北京
8.4.4 运算符
运算符是一个特殊的符号,用于告诉解释器执行特定的 数学 或 逻辑运算。Lua 提供了以下几种运算符类型:
- 算术运算符
- 关系运算符
- 逻辑运算符
- 其他运算符
⭐️(1)算术运算符
下表列出了 Lua 语言中的常用算术运算符,设定 A
的值为 10
, B
的值为 20
:
注意,
- SciTE 对 Lua 支持的目前最高版本为 5.1 ,而整除运算符
//
需要在 Lua5.3 版本以上,所以当前 SciTE 中无法看到效果。 - 在 命令行模式 中,直接输入变量名 回车,即相当于
print()
函数输出该变量。
⭐️(2)关系运算符
下表列出了 Lua 语言中的常用关系运算符,设定 A
的值为 10
, B
的值为 20
:
⭐️(3)逻辑运算符
注意, Lua 系统将 false
与 nil
作为 假,将 true
与 非nil
作为 真,即使是 0
也是真。
下表列出了Lua 语言中的常用 逻辑运算符,设定 A
的值为 true
, B
的值为 false
:
⭐️(4)其他运算符
下表列出了 Lua 语言中的 连接运算符 与 计算 表
或 字符串
长度 的运算符:
str = "abcdefg"
print(#str) -- 输出 7
8.4.5 函数
Lua 中函数的定义是以 function
开头,后跟 函数名 与 参数列表,以 end
结尾。其 可以没有返回值,也可以一次返回多个值。
⭐️(1)固定参函数
Lua 中的函数在调用时与 Java 语言中方法的调用是不同的,其 不要求实参的个数必须与函数中形参的个数相同。
- 如果实参个数少于形参个数,则系统自动使用
nil
填充; - 如果实参个数多于形参个数,多出的将被系统 自动忽略。
-- 定义一个普通函数,包含两个形参
function f(a, b)print(a, b)
end-- 无实参传递
f() -- 输出:nil nil-- 传递一个实参
f(10) -- 输出:10 nil-- 传递两个实参
f(10, 20) -- 输出:10 20-- 传递三个实参
f(10, 20, 30) -- 输出:10 20
⭐️(2)可变参函数
在函数定义时不给出具体形参的个数,而是使用 三个连续的点号。在函数调用时就可以向该函数传递任意个数的参数,函数可以全部接收。
-- 定义一个可变参函数
function f(...)local a,b,c,d = ...print(a, b, c, d)--print(...) -- 可以全部输出
end-- 传递三个实参
f(10, 20, 30) -- 输出:10 20 30 nil-- 传递四个实参
f(10, 20, 30, 40) -- 输出:10 20 30 40-- 传递五个实参
f(10, 20, 30, 40, 50) -- 输出:10 20 30 40
⭐️(3)可返回多个值
Lua 中的函数一次可以返回多个值,但需要有多个变量来同时接收。
-- 定义一个普通函数,返回两个值
function f(a, b)local sum = a + blocal mul = a * breturn sum, mul;
end-- 一次性接收两个值
m, n = f(3, 5)
print(m, n) -- 输出:8 15
⭐️(4)函数作为参数
Lua 的函数中,允许 函数 作为参数。而作为参数的函数,可以是已经定义好的 普通函数
,也可以是匿名函数
。
-- 定义两个普通函数
function sum(a, b)return a + b
endfunction mul(a, b)return a * b
end-- 定义一个函数,其参数为另一个参数
function f(m, n, fun)local result = fun(m, n)print(result)
end-- 调用
f(3, 5, sum) -- 输出:8
f(3, 5, mul) -- 输出:15-- 匿名函数调用
f(3, 5, function (a, b)return b - a;end
); -- 输出:2
8.4.6 流程控制语句
Lua 提供了 if
作为 流程控制语句。
⭐️(1)if 语句
Lua 提供了 if...then
用于表示条件判断,其中 if
的判断条件可以是 任意表达式。 Lua 系统将 false
与 nil
作为假,将 true
与 非nil
作为真,即使 是 0
也是真。
a = 5
if(a > 0) thenprint("num > 0")
elseprint("num <= 0")
end-- 输出: num > 0
需要注意,Lua 中的 if
语句的判断条件 可以使用小括号括起来,也可以不使用。
⭐️(2)if 嵌套语句
Lua 中提供了专门的关键字 elseif
来做 if
嵌套语句。注意,不能使用 else
与 if
两个关键字的联用形式 ,即不能使用 else if
来嵌套 if
语句。
a = 5
if(a > 0) thenprint("num > 0")
elseif a == 0 thenprint("num = 0")
elseprint("num < 0")
end
8.4.7 循环控制语句
Lua 提供了四种循环控制语句: while...do
循环、 repeat...until
循环、数值 for
循环,及 泛型 for
循环。同时, Lua 还提供了 break
与 goto
两种循环流程控制语句。
⭐️(1)while … do
只要 while
中的 条件成立 就一直循环。
a = 3
while a>0 doprint(a)a = a - 1 -- 注意:这里没有a--
end
输出:
3
2
1
⭐️(2)repeat … until
until
中的 条件成立了,循环就要 停止。
a = 3
repeatprint(a)a = a - 1 -- 注意:这里没有a--
until a <= 0
输出:
3
2
1
⭐️(3)数值 for
这种 for
循环只参用于循环变量为 数值型 的情况。其语法格式为:
for var=exp1, exp2, exp3 do循环体
end
var
为指定的 循环变量, exp1
为 循环 起始值, exp2
为 循环 结束值, exp3
为 循环 步长。
- 步长可省略不写,默认为
1
。 - 每循环一次,系统内部都会做一次当前循环变量
var
的值与exp2
的比较,如果var
小于等于exp2
的值,则继续循环,否则结束循环。
例如:
for i = 10, 50, 20 doprint(i)
end
输出:
10
30
50
⭐️(4)泛型 for
泛型 for
用于遍历 table
中的所有值,其需要与 Lua 的 迭代器
联合使用。后面 table 学习时再详解。
⭐️(5)break
break
语句可以提前终止循环。其只能用于循环之中。
for i = 1, 9 doprint(i)if i == 3 thenbreakend
end
输出:
1
2
3
⭐️(6)goto (不建议使用,不然可能使代码杂乱无章)
goto
语句可以将执行流程 无条件地跳转 到指定的标记语句处开始执行,注意,是开始执行,并非仅执行这一句,而是从这句开始后面的语句都会重新执行。当然,该标识语句在第一次经过时也是会执行的,并非是必须由 goto
跳转时才执行。
语句标记使用一对双冒号括起来,置于语句前面。goto
语句可以使用在循环之外。
function f(a)::flag:: print("=========")if a > 1 thenprint(a)a = a - 1goto flagend
endf(5)
输出:
=========
5
=========
4
=========
3
=========
2
=========
注意,Lua5.1 中 不支持 双冒号 的语句标记。
8.5 Lua 语法进阶
8.5.1 table
⭐️(1)数组
使用 table
可以定义 一维、二维、多维数组。不过,需要注意, Lua 中的数组索引是从 1
开始的,且 无需声明数组长度,可以随时增加元素。当然,同一数组中的 元素可以是任意类型。
-- 定义一个一维数组
cities = {"北京", "上海", "广州"}
cities[4] = "深圳"for i=1, 4 doprint("cities["..i.."]="..cities[i])
end
输出:
cities[1]=北京
cities[2]=上海
cities[3]=广州
cities[4]=深圳
-- 声明一个二维数组
arr = {} -- 必须声明空数组
for i= 1, 3 doarr[i] = {} -- 必须声明空数组for j = 1, 2 doarr[i][j] = i * jprint(arr[i][j])end
end
输出:
1
2
2
4
3
6
⭐️(2)map
使用 table
也可以定义出类似 map
的 key-value
数据结构。其可以定义 table
时直接指定 key-value
,也可单独指定 key-value
。而访问时,一般都是通过 table
的 key
直接访问,也可以数组索引方式来访问,此时的 key
即为索引。
例 1 :
-- 定义一个map
emp = {name = "张三", age = "23", depart = "销售部"}-- 通过下标方式操作
emp["gender"] = "男"
print(emp["name"]) -- 输出:张三
print(emp["gender"]) -- 输出:男-- 点号方式操作 (推荐)
emp.office = "2nd floor"
print(emp.age) -- 输出:23
print(emp.office) -- 输出:2nd floor
例 2 :
a = "xxx"
b = 3
c = 5-- 定义一个map,其key为表达式(需要用方括号括起来)
arr = {["emp_"..a] = true,["hi"] = 123,[b + c] = "hello",
}print(arr.emp_xxx) -- 输出:true
-- print(arr.8) -- 报错
print(arr.hi) -- 输出:123
print(arr[8]) -- 输出:hello
⭐️(3)混合结构
Lua 允许将数组与 key-value
混合在同一个 table
中进行定义。 key-value
不会占用数组的数字索引值。
emp = {"北京", name = "张三", "上海", age = "23", "广州", depart = "销售部", "深圳"}print(emp[1]) -- 输出:北京
print(emp[2]) -- 输出:上海
print(emp[3]) -- 输出:广州
print(emp[4]) -- 输出:深圳
常见使用方法:
-- 定义一个数组,map 为混合结构
emp = {{name="张三", age=23},{name="李四", age=24},{name="王五", age=25},{name="赵六", age=26},
}for i = 1, 4 doprint(emp[i].name.." : "..emp[i].age)
end
输出:
张三 : 23
李四 : 24
王五 : 25
赵六 : 26
⭐️(4)table 操作函数
Lua 中提供了对 table
进行操作的函数。
A、table.concat()
【函数】table.concat (table [, sep [, start [, end]]]):
【解析】该函数用于将指定的 table
数组元素 进行 字符串连接。连接从 start
索引位置到 end
索引位置的所有数组元素, 元素间使用指定的分隔符 sep
隔开。 如果 table
是一个混合结构,那么这个连接与 key-value
无关,仅是连接数组元素。
emp = {"北京", name = "张三", "上海", age = "23", "广州", depart = "销售部", "深圳"}print(table.concat(emp, ",")) -- 输出:北京,上海,广州,深圳
print(table.concat(emp, ",", 2, 3)) -- 输出:上海,广州
B、table.unpack()
【函数】table.unpack (table [, i [, j]])
【解析】拆包。该函数返回指定 table
的 数组 中的从第 i
个元素到第 j
个元素值。 i
与 j
是可选的,默认 i
为 1
, j
为数组的最后一个元素。 Lua5.1 不支持该函数。
arr = {"bj", "sh", "gz", "sz"}table.unpack(arr) -- 输出:bj sh gz sz
table.unpack(arr, 2, 3) -- 输出:sh gz-- 也可以使用变量接收
a, b, c, d = table.unpack(arr)
C、table.pack()
【函数】table. pack (...)
【解析】打包。该函数的参数是一个可变参,其可将指定的参数打包为一个 table
返回。这个返回的 table
中具有一个属性 n
,用于表示该 table
包含的 元素个数。 Lua5.1 不支持该函数。
t = table.pack("apple", "banana", "peach")
table.concat(t, ",") -- 输出:apple,banana,peach
D、table.maxn()
【函数】table.maxn(table)
【解析】该函数返回指定 table
的数组中的 最大索引值,即数组包含元素的个数。
emp = {"北京", name = "张三", "上海", age = "23", "广州", depart = "销售部", "深圳"}print(table.concat(emp, ",")) -- 输出:北京,上海,广州,深圳print(table.maxn(emp)) -- 输出:4
E、table.insert()
【函数】table.insert (table, [pos,] value):
【解析】该函数用于在指定 table
的数组部分指定位置 pos
插入值为 value
的一个元素 。 其后的元素会被后移 。 pos
参数可选 默认为数组部分末尾 。
cities = {"北京", "上海", "广州"}table.insert(cities, 2, "深圳")
table.insert(cities, "天津")print(table.concat(cities, ",")) -- 输出:北京,深圳,上海,广州,天津
F、table.remove()
【函数】table.remove (table [, pos])
【解析】该函数用于 删除并返回 指定 table
中数组部分位于 pos
位置的元素 。 其后的元素会被前移 。 pos
参数可选默认删除数组中的最后一个元素。
cities = {"北京", "上海", "广州", "深圳", "天津"}table.remove(cities, 2)
table.remove(cities)print(table.concat(cities, ",")) -- 输出:北京,广州,深圳
G、table.sort()
【函数】table. sort(table [,fun(a,b)])
【解析】该函数用于对指定的 table
的数组元素进行 默认 升序排序,也可按照指定函数 fun(a,b)
中指定的规则进行排序。 fun(a,b)
是一个用于比较 a
与 b
的函数, a
与 b
分别代表数组中的两个相邻元素。
cities = {"bj北京", "sh上海", "gz广州", "sz深圳", "tj天津"}table.sort(cities, function(a, b) -- 降序return a > b -- 如果相邻的两个为真,保持原来的队形end
)print(table.concat(cities, ",")) -- 输出:tj天津,sz深圳,sh上海,gz广州,bj北京
注意:
- 如果
arr
中的元素既有 字符串 又有 数值型 ,那么对其进行排序会 报错。- 如果数组中多个元素相同,则其相同的多个元素的排序结果不确定,即这些元素的索引谁排前谁排后,不确定。
- 如果数组元素中包含
nil
,则排序会 报错。
8.5.2 迭代器
Lua 提供了两个迭代器 pairs(table)
与 ipairs(table)
。这两个迭代器通常会应用于 泛型 for
循环中,用于遍历指定的 table
。这两个迭代器的不同是:
ipairs(table)
:仅会迭代指定 table 中的 数组元素。pairs(table)
:会迭代 整个 table 元素 ,无论是 数组元素,还是key-value
。
emp = {"北京", name = "张三", "上海", age = "23", "广州", depart = "销售部", "深圳"}-- 遍历emp中的所有数组元素
for i, v in ipairs(emp) doprint(i, v)
end
--[[输出:
1 北京
2 上海
3 广州
4 深圳
--]]-- 遍历emp中的所有元素
for k, v in pairs(emp) doprint(k, v)
end
--[[输出:
1 北京
2 上海
3 广州
4 深圳
depart 销售部
name 张三
age 23
--]]
8.5.3 模块
模块是 Lua 中特有的一种数据结构。 从 Lua 5.1 开始, Lua 加入了标准的 模块管理机制,可以把一些公用的代码放在一个文件里,以 API 接口 的形式在其他地方调用,有利于代码的重用和降低代码耦合度。
模块文件主要由 table
组成。在 table
中添加相应的变量、函数,最后文件返回该 table
即可。如果其它文件中需要使用该模块,只需通过 require
将该 模块导入 即可。
⭐️(1)定义一个模块
模块 是一个 lua 文件,其中会包含一个 table
。一般情况下该文件名与该 table
名称相同,但其 并不是必须的。
例如: 定义rectangle
模块, 创建一个rectangle.lua
文件
-- 声明一个模块
rectangle = {}-- 为模块添加一个变量
rectangle.pi = 3.14-- 为模块添加函数(求周长)
function rectangle.perimeter(a, b)return (a + b) * 2
end-- 以匿名函数方式为模块添加一个函数(求面积)
rectangle.area = function(a, b)return a * b
end-- ================= 定义与模块无关的内容===============
-- 定义一个全局变量
goldenRatio = 0.618-- 定义一个局部函数(求圆的面积)
local function circularArea(r)return rectangle.pi * r * r
end-- 定义一个全局函数(求矩形中最大圆的面积)
function maxCircularArea(a, b)local r = math.min(a, b)return circularArea(r / 2)
endreturn rectangle
⭐️(2)使用模块
这里要用到一个函数 require("文件路径"))
,其中文件名是 不能写 .lua
扩展名的。该函数可以将指定的 lua 文件静态导入(合并为一个文件)。不过需要注意的是,该函数的使用可以省略小括号,写为 require"文件路径"
。
-- 导入一个模块
require "rectangle"-- 访问模块的属性,调用模块的函数
print(rectangle.pi) -- 输出:3.14
print(rectangle.perimeter(3, 5)) -- 输出:16
print(rectangle.area(3, 5)) -- 输出:15
require()
函数是有返回值的,返回的就是模块文件最后 return
的 table
。可以使用一个变量接收该 table
值 作为模块的别名,就可以 使用 别名 来访问模块了。
-- 导入一个模块
rect = require "rectangle"-- 访问模块的属性,调用模块的函数
print(rect.pi) -- 输出:3.14
print(rect.perimeter(3, 5)) -- 输出:16
print(rect.area(3, 5)) -- 输出:15
⭐️(3)再看模块
模块文件中一般定义的 变量 与 函数 都是模块 table
相关内容,但也可以定义其它与 table
无关的内容。这些 全局变量与函数 就是 普通的全局变量与函数,与模块无关,但会随着模块的导入而同时导入。所以在使用时可以直接使用,而无需也不能添加模块名称。
-- 导入一个模块
require "rectangle"-- 访问模块中与模块无关的内容
print(goldenRatio) -- 输出:0.618
print(maxCircularArea(4, 5)) -- 输出:12.56
-- print(circularArea(2)) -- 报错,局部的不能访问
8.5.4 元表和元方法
元表,即 Lua 中 普通 table
的 元数据表,而 元方法 则是元表中定义的普通表的默认行为。Lua 中的每个 普通 table
都可为其定义一个元表,用于 扩展 该 普通 table
的 行为功能。例如,
- 对于
table
与数值相加的行为, Lua 中是没有定义的,但用户可通过为其指定 元表 来 扩展这种行为; - 再如,用户访问不存在的
table
元素, Lua 默认返回的是nil
,但用户可能并不知道发生了什么。此时可以通过为该table
指定元表 来扩展 该行为:给用户提示信息,并返回用户指定的值。
⭐️(1)重要函数
元表 中有 两个重要函数
setmetatable(table, metatable)
:将metatable
指定为普通表table
的元表。getmetatable(table)
:获取指定普通表table
的元表。
⭐️(2)__index 元方法
当用户在对 table
进行 读取 访问时,如果 访问 的数组 索引 或 key
不存在,那么系统就会 自动调用 元表的 _ _index
元方法。该重写的方法可以是一个函数,也可以是另一个表。
- 如果重写的
_ _index
元方法是函数,且有返回值,则直接返回; - 如果 没有返回值,则返回
nil
。
例1:重写的方法是一个函数
emp = {"北京", nil, name = "张三", "上海", age = "23", "广州", depart = "销售部", "深圳"}print(emp.x)-- 声明一个元表
meta = {};-- 将原始表和元表相关联
setmetatable(emp, meta)-- 有返回值的情况
meta.__index = function(tab, key)return "通过【"..key.."】访问的值不存在"
end--~ -- 无返回值的情况
--~ meta.__index = function(tab, key)
--~ print("通过【"..key.."】访问的值不存在")
--~ endprint(emp.x)
print(emp[2])
输出:
nil
通过【x】访问的值不存在
通过【2】访问的值不存在
例2:重写的方法是一个表
emp = {"北京", name = "张三", "上海", age = "23", "广州", depart = "销售部", "深圳"}print(emp[5])-- 声明一个元表
meta = {};-- 将原始表和元表相关联
setmetatable(emp, meta)-- 再定义一个普通表
other = {}other[5] = "天津"
other[6] = "西安"-- 指定元表为另一个普通表
meta.__index = other-- 在原始表中若找不到,则会到元表指定的普通表中查找
print(emp[5])
输出:
nil
天津
⭐️(3)__newindex 元方法
当用户为 table
中一个 不存在 的索引或 key
赋值 时,就会自动调用元表的 _ _newindex
元方法。该重写的方法可以是一个函数,也可以是另一个表。
- 如果重写的
_ _newindex
元方法是函数,且有返回值,则直接返回; - 如果没有返回值,则返回
nil
。
⭐️(4)运算符 元方法
如果要为一个表扩展加号(+
)、减号(-
) 、等于(==
) 、小于(<
) 等运算功能,则可重写 相应的元方法。
例如,如果要为一个 table
扩展 加号(+
) 运算功能,则可重写该 table
元表的 _ _add
元方法,而具体的运算规则,则是定义在该重写的元方法中的。这样,当一个 table
在进行加法运算时,就会自动调用其元表的 _ _add
元方法。
类似于加法操作的其它操作,Lua 中还包含很多:
⭐️(5)__tostring 元方法
直接输出一个 table
,其输出的内容为类型与 table
的存放地址。如果想让其输出 table
中的内容,可重写 _ _tostring
元方法。
⭐️(6)__call 元方法
当将一个 table
以函数形式来使用时,系统会自动调用重写的 _ _call
元方法。该用法主要是可以简化对 table
的相关操作,将对 table
的操作与函数直接相结合。
⭐️(7)元表单独定义
为了便于 管理 与 复用,可以将元素单独定义为一个文件。该文件中 仅 可定义 一个元表,且一般文件名与元表名称相同。
若一个文件要使用其它文件中定义的元表,只需使用 require
元表文件名 即可将元表导入使用。
如果用户想扩展该元表而又不想修改元表文件,则可在用户 自己文件中 重写其相应功能 的 元方法 即可。
8.5.5 面向对象
Lua 中没有类的概念,但通过 table
、 function
与 元表 可以模拟和构造出具有 类这样功能的结构。
⭐️(1)简单对象的创建
Lua 中通过 table
与 function
可以创建出一个简单的 Lua 对象:
table
为 Lua 对象赋予 属性;- 通过
function
为 Lua 对象赋予 行为,即 方法。
⭐️(2)类的创建
Lua 中使用 table
、 function
与 元表 可以定义出类:
- 使用一个 表 作为 基础类,使用一个
function
作为该基础类的new()
方法。 - 在该
new()
方法中 创建一个空表,再为该 空表 指定一个元表。 - 该元表 重写
_ _index
元方法,且将基础表指定为重写的_ _index
元方法。 - 由于
new()
中的表是空表,所以用户访问的所有key
都会从基础类(表)中查找。
8.5.6 协同线程与协同函数
⭐️(1)协同线程
Lua 中有一种 特殊的线程,称为 coroutine
协同线程,简称 协程。其可以在运行时 暂停执行,然后转去执行其它线程,然后还可返回再继续执行没有执行完毕的内容。即可以“走走停停,停停再走走”。
协同线程 也称为 协作多线程,在Lua 中表示 独立的执行线程。 任意时刻只会有一个协程执行,而不会出现多个协程同时执行的情况。
协同线程的类型为 thread
,其启动、暂停、重启等,都需要通过函数来控制。下表是用于控制协同线程的基本方法。
⭐️(2)协同函数
协同线程 可以 单独 创建执行,也可以通过 协同函数 的 调用 启动执行。使用 coroutine
的 wrap()
函数创建的就是协同函数,其类型为 function
。
由于协同函数的本质就是函数,所以协同函数的调用方式就是标准的 函数调用方式。只不过,协同函数的调用会启动其内置的协同线程。
8.5.7 文件IO
⭐️(1)常用静态函数
A、io.open()
【格式】io.open (filename [, mode])
【解析】以 指定模式 打开指定文件,返回要打开文件的 句柄,就是一个对象(后面会讲 Lua 中的对象)。其中模式 mode
有三种,但同时还可配合两个符号使用:
r
:只读,默认模式w
:只写,写入内容会覆盖文件原有内容a
:只写,以追加方式写入内容+
:增加符,在r+
、w+
、a+
均变为了 读写b
:二进制表示符。如果要操作的文件为二进制文件,则需要变为rb
、wb
、ab
。
B、io.input()
【格式】io.input (file)
【解析】指定要读取的文件。
C、io.outout()
【格式】io.output (file)
【解析】指定要写入的文件。
D、io.read()
【格式】io.read([format])
【解析】以指定格式读取 io.input()
中指定的输入文件。其中 format
格式有:
*l
:从当前位置的 下一个位置 开始读取 整个行,默认格式*n
:读取 下一个数字,其将作为浮点数或整数*a
:从当前位置的 下一个位置 开始读取 整个文件number
:这是一个数字,表示要 读取的字符的个数
E、io.write()
【格式】io.write(data)
【解析】将指定的数据 data
写入到 io.output()
中指定的输出文件。
⭐️(2)常用实例函数
A、file:read()
这里的 file
使用的是 io.open()
函数返回的 file
,其实际就是 Lua 中的一个对象。其用法与 io.read()
的相同。
B、file:write()
用法与 io.write()
的相同。
C、file:seek()
【格式】file:seek ([whence [, offset]])
【解析】该函数用于获取或设置文件读写指针的当前位置。
位置从 1 开始计数,除文件最后一行外,每行都有行结束符,其会占两个字符位置。位置 0 表示文件第一个位置的前面位置。
当seek() 为无参时会返回读写指针的当前位置。参数 whence 的值有三种,表示将指针定位的不同位置。而 offset 则表示相对于 whence 指定位置的偏移量, offset 的默认值为 0 为正表示向后偏移,为负表示向前偏移。
- set :表示将指针定位到文件开头处,即 0 位置处
- cur :表示指针保持当前位置不变,默认值
- end :表示将指针定位到文件结尾处