八、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 + b
local mul = a * b
return sum, mul;
end
-- 一次性接收两个值
m, n = f(3, 5)
print(m, n) -- 输出:8 15
⭐️(4)函数作为参数
Lua 的函数中,允许 函数 作为参数。而作为参数的函数,可以是已经定义好的 普通函数
,也可以是匿名函数
。
-- 定义两个普通函数
function sum(a, b)
return a + b
end
function 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) then
print("num > 0")
else
print("num <= 0")
end
-- 输出: num > 0
需要注意,Lua 中的 if
语句的判断条件 可以使用小括号括起来,也可以不使用。
⭐️(2)if 嵌套语句
Lua 中提供了专门的关键字 elseif
来做 if
嵌套语句。注意,不能使用 else
与 if
两个关键字的联用形式 ,即不能使用 else if
来嵌套 if
语句。
a = 5
if(a > 0) then
print("num > 0")
elseif a == 0 then
print("num = 0")
else
print("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 do
print(a)
a = a - 1 -- 注意:这里没有a--
end
输出:
3
2
1
⭐️(2)repeat … until
until
中的 条件成立了,循环就要 停止。
a = 3
repeat
print(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 do
print(i)
end
输出:
10
30
50
⭐️(4)泛型 for
泛型 for
用于遍历 table
中的所有值,其需要与 Lua 的 迭代器
联合使用。后面 table 学习时再详解。
⭐️(5)break
break
语句可以提前终止循环。其只能用于循环之中。
for i = 1, 9 do
print(i)
if i == 3 then
break
end
end
输出:
1
2
3
⭐️(6)goto (不建议使用,不然可能使代码杂乱无章)
goto
语句可以将执行流程 无条件地跳转 到指定的标记语句处开始执行,注意,是开始执行,并非仅执行这一句,而是从这句开始后面的语句都会重新执行。当然,该标识语句在第一次经过时也是会执行的,并非是必须由 goto
跳转时才执行。
语句标记使用一对双冒号括起来,置于语句前面。goto
语句可以使用在循环之外。
function f(a)
::flag:: print("=========")
if a > 1 then
print(a)
a = a - 1
goto flag
end
end
f(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 do
print("cities["..i.."]="..cities[i])
end
输出:
cities[1]=北京
cities[2]=上海
cities[3]=广州
cities[4]=深圳
-- 声明一个二维数组
arr = {} -- 必须声明空数组
for i= 1, 3 do
arr[i] = {} -- 必须声明空数组
for j = 1, 2 do
arr[i][j] = i * j
print(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 do
print(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) do
print(i, v)
end
--[[输出:
1 北京
2 上海
3 广州
4 深圳
--]]
-- 遍历emp中的所有元素
for k, v in pairs(emp) do
print(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)
end
return 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.."】访问的值不存在")
--~ end
print(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
。
例1:重写的方法是一个函数
emp = {"北京", name = "张三", "上海", age = "23", "广州", depart = "销售部", "深圳"}
-- 声明一个元表
meta = {};
-- 将原始表和元表相关联
setmetatable(emp, meta)
-- 无返回值的情况 (有返回值的意义不大)
function meta.__newindex(tab, key, value)
print("新增的key为:"..key..",value为:"..value)
-- 将新增的 key-value 写入到原始表
rawset(tab, key, value)
end
emp.x = "天津"
print(emp.x)
输出:
新增的key为:x,value为:天津
天津
例2:重写的方法是一个表
emp = {"北京", name = "张三", "上海", age = "23", "广州", depart = "销售部", "深圳"}
-- 声明一个元表
meta = {};
-- 将原始表和元表相关联
setmetatable(emp, meta)
-- 再定义一个普通表
other = {}
-- 元表指定的另一个普通表的作用是,暂存新增加的数据
meta.__newindex = other
emp.x = "天津"
print(emp.x)
print(other.x)
输出:
nil
天津
⭐️(4)运算符 元方法
如果要为一个表扩展加号(+
)、减号(-
) 、等于(==
) 、小于(<
) 等运算功能,则可重写 相应的元方法。
例如,如果要为一个 table
扩展 加号(+
) 运算功能,则可重写该 table
元表的 _ _add
元方法,而具体的运算规则,则是定义在该重写的元方法中的。这样,当一个 table
在进行加法运算时,就会自动调用其元表的 _ _add
元方法。
emp = {"北京", name = "张三", "上海", age = 23, "广州", depart = "销售部", 12, "深圳"}
-- 声明一个元表
meta = {
__add = function(tab, num)
-- 遍历tab中的所有元素
for k, v in pairs(tab) do
-- 若value为数值类型,则做算术加法
if type(v) == "number" then
tab[k] = v + num
-- 若value为string,则做字符串拼接
elseif type(v) == "string" then
tab[k] = v..num
end
end
-- 返回变化后的table
return tab
end
};
-- 将原始表和元表相关联
setmetatable(emp, meta)
empsum = emp + 5
for k, v in pairs(empsum) do
print(k..":"..v)
end
输出:
1:北京5
2:上海5
3:广州5
4:17
5:深圳5
depart:销售部5
name:张三5
age:28
类似于加法操作的其它操作,Lua 中还包含很多:
⭐️(5)__tostring 元方法
直接输出一个 table
,其输出的内容为类型与 table
的存放地址。如果想让其输出 table
中的内容,可重写 _ _tostring
元方法。
emp = {"北京", name = "张三", "上海", age = 23, "广州", depart = "销售部", 12, "深圳"}
-- 声明一个元表
meta = {
__add = function(tab, num)
-- 遍历tab中的所有元素
for k, v in pairs(tab) do
-- 若value为数值类型,则做算术加法
if type(v) == "number" then
tab[k] = v + num
-- 若value为string,则做字符串拼接
elseif type(v) == "string" then
tab[k] = v..num
end
end
-- 返回变化后的table
return tab
end, -- 注意!!! 有多个元方法时,这里必须要添加一个逗号
__tostring = function(tab)
str = ""
-- 字符串拼接
for k, v in pairs(tab) do
str = str.." "..k..":"..v
end
return str
end
};
-- 将原始表和元表相关联
setmetatable(emp, meta)
empsum = emp + 5
print(emp)
print(empsum)
输出:
1:北京5 2:上海5 3:广州5 4:17 5:深圳5 depart:销售部5 name:张三5 age:28
1:北京5 2:上海5 3:广州5 4:17 5:深圳5 depart:销售部5 name:张三5 age:28
⭐️(6)__call 元方法
当将一个 table
以函数形式 来使用时,系统会自动调用重写的 _ _call
元方法。该用法主要是可以简化对 table
的相关操作,将对 table
的操作 与 函数 直接相结合。
emp = {"北京", name = "张三", "上海", age = 23, "广州", depart = "销售部", 12, "深圳"}
-- 将原始表和匿名元表相关联
setmetatable(emp, {
__call = function(tab, num, str)
-- 遍历tab中的所有元素
for k, v in pairs(tab) do
-- 若value为数值类型,则做算术加法
if type(v) == "number" then
tab[k] = v + num
-- 若value为string,则做字符串拼接
elseif type(v) == "string" then
tab[k] = v..str
end
end
-- 返回变化后的table
return tab
end
})
newemp = emp(5, "-Lua")
for k, v in pairs(newemp) do
print(k..":"..v)
end
输出:
1:北京-Lua
2:上海-Lua
3:广州-Lua
4:17
5:深圳-Lua
depart:销售部-Lua
name:张三-Lua
age:28
如果
__call
用好了,可以大大减少我们的工作量!!!
⭐️(7)元表单独定义
为了便于 管理 与 复用,可以将元素单独定义为一个文件。该文件中 仅 可定义 一个元表,且一般文件名与元表名称相同。例如新建一个 meta.lua
文件,文件内容如下:
-- 声明一个元表
meta = {
__add = function(tab, num)
-- 遍历tab中的所有元素
for k, v in pairs(tab) do
-- 若value为数值类型,则做算术加法
if type(v) == "number" then
tab[k] = v + num
-- 若value为string,则做字符串拼接
elseif type(v) == "string" then
tab[k] = v..num
end
end
-- 返回变化后的table
return tab
end, -- 注意!!! 有多个元方法时,这里必须要添加一个逗号
__tostring = function(tab)
str = ""
-- 字符串拼接
for k, v in pairs(tab) do
str = str.." "..k..":"..v
end
return str
end
};
若一个文件要使用其它文件中定义的元表,只需使用 require
元表文件名 即可将元表导入使用。
require "meta"
emp = {"北京", name = "张三", "上海", age = 23, "广州", depart = "销售部", 12, "深圳"}
-- 将原始表和元表相关联
setmetatable(emp, meta)
empsum = emp + 5
print(emp)
print(empsum)
meta.__index = function(tab, key) --在自己文件中重下
return "通过【"..key.."】访问的值不存在"
end
print(emp.x)
如果用户想扩展该元表而又不想修改元表文件,则可在用户 自己文件中 重写其相应功能 的 元方法 即可。
8.5.5 面向对象
Lua 中没有类的概念,但通过 table
、 function
与 元表 可以模拟和构造出具有 类这样功能的结构。
⭐️(1)简单对象的创建
Lua 中通过 table
与 function
可以创建出一个简单的 Lua 对象:
table
为 Lua 对象赋予 属性;- 通过
function
为 Lua 对象赋予 行为,即 方法。
-- 创建一个名称为animal的对象
animal = {name = "Tom", age = 5}
-- 添加方法,推荐使用方式1
-- 方式1:冒号中会自动包含一个self 参数,表示当前对象本身,相当于this
function animal:bark(voice)
print(self.name.."在"..voice.."叫")
end
-- 方式2:该方式不能使用冒号:,来省略参数中的self
--~ animal.bark = function(self, voice)
--~ print(self.name.."在"..voice.."叫")
--~ end
-- =============== 使用对象 =======================
animal:bark("喵喵") -- 方式1,调用也是将 (点号) 换成 (冒号)
--~ animal.bark(animal, "喵喵") -- 方式2
-- 另定义一个对象,指向相同地址
animal2 = animal
-- 将 animal 置空
animal = nil
animal2.name = "Jerry"
animal2:bark("吱吱") -- 方式1
--~ animal2.bark(animal2, "吱吱") -- 方式2
⭐️(2)类的创建
Lua 中使用 table
、 function
与 元表 可以定义出类:
- 使用一个 表 作为 基础类,使用一个
function
作为该基础类的new()
方法。 - 在该
new()
方法中 创建一个空表,再为该 空表 指定一个元表。 - 该元表 重写
_ _index
元方法,且将基础表指定为重写的_ _index
元方法。 - 由于
new()
中的表是空表,所以用户访问的所有key
都会从基础类(表)中查找。
-- 创建一个类
Animal = {name = "no_name", age = 0} -- 基础类表
function Animal:bark(voice)
print(self.name.."在"..voice.."叫")
end
-- 为该类添加一个无参构造器
function Animal:new()
-- 创建一个空表
local a = {} -- 局部的
-- 为该空表a指定元表为当前基础类
-- 在该空表a中找不到的key,会从self基础类表中查找
setmetatable(a, {__index = self}) -- __index 只有在读的时候才会涉及
-- 返回空表
return a
end
--============== 创建对象=================
animal = Animal:new()
animal2 = Animal:new() -- 此时为两个对象
-- 下面这三个属性,是在表a中,和基础类表没任何关系
animal.name = "Tom" -- 写操作
animal.age = 8
animal.type = "猫"
-- 直接从表a中读
print(animal.name.."今年"..animal.age.."岁了,它是一只"..animal.type)
function animal2:skill() -- 写操作,也可以理解为键值对,key为方法名,value为函数
return "小老鼠,会偷油"
end
-- 在自己的表a中查找没找到,则在基础类表中查找
print(animal2.name.."今年"..animal2.age.."岁了,它是一只"..animal2.skill())
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 :表示将指针定位到文件结尾处