Lua 特性
交互式/脚本式编程
交互式
在终端中通过 lua
或者 lua -i
命令直接启用交互式编程模式,每一行代码在回车 后都将输出结果,并开启下一行的待输入。
lua -i
脚本式
创建以 .lua
结尾的文件,在其中编写代码后统一执行。
print("Hello World!")
print("https://lyrikp.art")
注释
单行注释 -- 注释内容
:
-- 这里写注释
多行注释 --[[注释内容]]--
:
--[[
注释1
注释2
]]--
Lua 变量声明与赋值
- 变量名区分大小写
- 默认情况下变量为全局的,且不需要声明
- 为赋值情况下值为
nil
- 为赋值情况下值为
- 作用域变量在赋值时前加
local
a = 1 -- 赋值即声明
local b = 2 -- 当前作用域声明
print(a)
-- 1
print(c)
-- nil
多重赋值
-- 多重赋值
a, b = 1, 2
print(a, b)
-- 1 2
-- 赋值不够变量数量,默认为 nil
a, b, c = 1, 2
print(a, b, c)
-- 1 2 nil
Lua 数据类型
数据类型 | 描述 |
---|---|
nil | 无效值、空值,等于 false |
boolean | 布尔值,值为 true(0也是) 和 false(nil和false), |
number | 双精度浮点数 |
string | 字符串 引号表示,[[表示一块字符串]] |
function | 函数 |
userdata | 任意存储在变量中的 C 数据结构 |
thread | 独立线路,用于执行协同程序 |
table | 关联数组 |
nil 空
未赋值的变量值为 nil,在进行比较的时候应该加上引号,因为 type()
函数返回的是字符串。
type(x) == 'nil' -- true
boolean 布尔
boolean 类型只有 true(真)、false(假)两个可选值,其只将 false 和 nil 看做 false,其余都为 true(包括数字0)。
在 Lua 中,不等号为 ~=
。
与 and
或 or
非 not
a = 0 -- 真
b = nil -- 假
-- 如果 b>10 真,返回yes
print(b > 10 and "yes" or "no") -- no
number 数值
Lua 中只有一种 number 类型,且为双精度实数浮点数。
- 支持 16 进制表示
0x11
- 支持科学计数法
2e10
a = 0x11
b = 2e10
print(a, b)
-- 17 20000000000.0
运算符
- 支持乘幂
^
- 支持移位符号
<<
>>
print(2^3)
-- 8
print(1<<3)
-- 8
string 字符串
跳转 常用 string 库。
字符串序号
A | B | C | D | 1 | 2 | 3 | 4 | ! | ! |
---|---|---|---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
-10 | -9 | -8 | -7 | -6 | -5 | -4 | -3 | -2 | -1 |
在切片的时候首尾序号索引都会包括在内。
单行和多行字符串
字符串由单引号或者双引号表示,在由换行的时候,可以用 [[]]
表示一块字符串
- 支持转义字符
\n
string1 = "字符串1"
string2 = "字符串2"
string3 = [[
这里
所有行
\n视为一个字符串整体
带换行的
]]
-- 转义字符不会被转义,输出的是原始值
string4 = "abc\ndef"
--[[
abc
def
]]--
数字字符串运算
如果在对数字字符串进行算术操作的时,Lua 会自动尝试将其转化为数字:
print("2" + 6)
-- 8.0
拼接字符串
数字和字符串都可以拼接,使用 ..
进行拼接。
print("a" .. "b")
-- ab
print(157 .. 428)
-- 157428
字符串长度
Lua 使用 #
快速返回字符串长度:
len = "示例字符串"
print(#len)
-- 15
print(#"Hello World!")
-- 12
字符数字转换
使用 tostring()
函数与 tonumber()
函数转换字符串与数值的类型,字符串到数字无法转换时,输出结果为 nil
。
a = "abc"
b = tostring(10)
c = tonumber(a)
print(a)
-- "abc"
print(b)
-- "10"
print(c)
-- nil
ASCII码 转换
实际值 1 对应 ASCII码 49(0x31)
使用 string.char(ASCII码)
将 ASCII码 → 实际值
s = string.char(0x30, 0x31, 0x32, 0x33)
print(s)
-- 0 1 2 3
-- 是 HEX 的数值
print(string.char(49))
-- 1
使用 string.byte(实际值, 索引, 索引)
实际值 → ASCII码
n = string.byte(s, 2)
print(n)
-- 49 | 去出了 0x31
print(string.byte(1))
-- 49
Lua 字符串中 0, 0x00 不会截断字符串,会正常计入字符串长度。
table 表
table 通过“构造表达式”来完成,即 {}
来创建空壳,或者填入数据直接初始化。
默认情况下,table 表的索引下标为数字,table 下标从 1 开始。也可以设定键值对来改变下标的形式。
table 表是一个关联数组,各种数据类型都能存进去,如数值、字符、函数和 table 本身。
-- 创建空表
local tbl1 = {}
-- 初始化表
local tbl2 = {1, "ac", {}, function() end}
table 表长自动增长的,未初始化时值为 nil。
插入元素
用 table.insert(表名, 插入位置, 插入物)
来给一个表插入元素,当未指定插入位置时,默认插到表尾。
table 表可以自动增长,因此在给表尾后一个为止赋值可以直接插入元素到末尾。
a = {1, "ac", {}, function() end}
a[5] = 123 -- 直接插入到表尾位置
-- {1, "ac", {}, function() end, 123}
table.insert(a, "d") -- 在表尾插入 "d"
-- {1, "ac", {}, function() end, 123, "d"}
table.insert(a, 2, "f") -- 在第二个位置插入 "f"
-- {1, "f", "ac", {}, function() end, 123, "d"}
移除元素
用 table.remove(表名, 移除位置)
移除表中的元素。
a = {1, "ac", {}, function() end}
table.remove(a, 2)
-- {1, {}, function() end}
键值 table
用字符串作为表的下标,相当于 Python 中的字典键值对。
变量键直接用形如 a = 1
n = "2"
即可;字符串下标则需要用中括号包裹,形如 ["moon" = "🌕"]
["cheese" = "🧀"]
。
a = {
a = 1,
b = "1234",
[",;"] = 123
}
print(a.b)
-- "1234"
print(a[",;"])
-- 123
出现字符下标时,a["b"]
和 a.b
都是合法的,其输出值均为 "1234"
;但是当表中为数字索引(没有字符下标的情况)时,第二种引用不合法。
直接用下标就可以为表进行字符串下标的赋值,也可以直接增加没有的下标。
a["abc"] = "abcdefg"
循环遍历键值对 table 需要用到 pairs(表名)
函数来表明其含有键值对
for k,v in pairs(a) do
print(k..": "..v)
end
-- a: 1
-- b: "1234"
-- ",;": 123
-- "abc": "abcdefg"
函数 function
函数实际上也可以看做是一个数据类型,其操作与其他数据类型基本等价。
函数声明由 function 函数名(参数)
和 end
组成结构,二者之间是函数内容。
也有 函数名 = function()
+ end
的形式,不常用。
function f(a)
-- 函数内容
end
默认的返回值为 nil
。
first-class
Lua 中函数是“第一类值”,可以存在变量里,也可以用函数去调用其他函数,即直接将函数视为参数。
-- 阶乘函数
function factorial1(n)
if n == 0 then
return 1
else
return n * factorial1(n-1)
end
end
print(factorial1(5))
-- 120
factorial2 = factorial1 -- 函数运算方式可以直接赋给另外一个
print(factorial2(5))
-- 120
全局表 _G
Lua 里有一个特殊的 table 表 _G
,用来保存所有的全局变量。
a = 1
print(_G["a"])
-- 1
所有的全局变量都在 _G
表中,包括我们所使用的函数,例如使用的方法 table.insert
:
print(_G["table"]) -- table: 0x10
print(_G["table"]["insert"]) -- function: 0xc1
这在多文件调用中经常使用。
分支判断
Lua 中的分支由 if 条件 then
开始 以 end
结束。支持 elseif
和 else
。
if 1>10 then
print("1>10")
elseif 1<10 then
print("1<10")
else
print("no")
end
循环
Lua 中有三种循环:
for do
循环while do
循环repeat until
循环
Lua 中不存在 a++
和 a += 1
这种形式的语句
for 循环
for 循环使用 for 条件 do
和 end
作为结构,其条件中定义的变量是作用域变量,只在该循环中生效。
条件格式为 变量 = 开始, 结束, 步长
,其包含开始和结束的数字,即若为 1~10,则 i 会从 1 开始递增到 10,不会到 11.
for i = 1,5 do
print(i)
end
-- 1 2 3 4 5
-- 设定步长
for i = 1,10,2 do
print(i)
end
-- 1 3 5 7 9
-- 步长也可以是负数
for i = 10, 1, -1 do
print(i)
end
-- 10 9 8 7 6 5 4 3 2 1
需要注意的是,在循环过程中,其用于循环计数的条件变量,即上述代码的 i
是不会被手动修改的,如果在循环过程中为 i
赋值,其会自动新建一个新的 local i
以供循环内使用。
for i = 1, 5 do
print(i)
i = 2
end
-- 1 2 3 4 5
-- 输出仍然不变,其自动创建了一个 local i = 2
while 循环
while 循环的结构为 while 条件 do
和 end
构成。
local n = 10
while n>1 do
n = n-1
print(n)
end
-- 9 8 7 6 5 4 3 2
可以使用 break
来打断循环直接退出。
local n = 10
while n>1 do
print(n)
n = n-1
if n == 5 then
break -- 使用 break 直接退出循环
end
end
-- 10 9 8 7 6
repeat 循环
repeat 循环的结构为 repeat 语句 until(条件)
,类似于 do-while 结构。
local a = 0
repeat
print(a)
a = a + 1
until(a > 5)
-- 0 1 2 3 4 5
string库
字符串库 string 是最常用的库之一,以下记录经常使用的接口。
接口的常用方法为 库.接口(对象, 参数)
。
string 是一个 metatable,因此可以直接对定义的string类型变量使用冒号语法糖,即
变量:函数(参数)
ASCII码转换 byte char
string.byte()
将 字符串 → ASCII码,可以设定索引范围,默认为 string.byte(s, 1, -1)
string.char()
将 ASCII码 → 字符串。
切分串 sub
string.sub(字符串, 首位, 末位)
接口返回首位到末位(包括末位)索引位置的子字符串。
其中 索引 为 负数 时表示倒数,末尾 不出现时默认为 -1,即最后位置。
print(string.sub("Hello Lua", 4, 7))
-- o Lu | 共 4 个
print(string.hub("Hello Lua", -3, -1))
-- Lua
可以使用 字符串名:sub(首位, 末位)
的形式简化语法
s = "12345"
s1 = s:sub(2,4)
-- 234
复制 rep
string.rep(字符串, 复制次数)
返回字符串的次数次拷贝。
local x = "abc"
print(string.rep("abc", 3))
print(x:rep(3))
-- abcabcabc
这里次数参数为总共出现的次数,并非多出来的次数。
求长度 len
string.len(字符)
求得字符串的长度,等价于 #字符
x = "Hello Lua"
print(string.len("Hello Lua"))
print(x:len())
-- 9
-- 等价于 #
print(#"Hello Lua")
-- 9
大小写 upper lower
string.lower(字符)
将其中的大写转换为小写。
string.upper(字符)
将其中的小写转换为大写。
s = "Hello Lua"
print(string.upper(s))
-- HELLO LUA
print(string.lower(s))
-- hello lua
-- 简便语法
a = s:upper()
b = s:lower()
该接口方法的转变是暂时的,并不会更改原来的变量。如果没有保存返回值,结果会被丢弃。
格式化 format
string.format("格式化参数", 字符)
依据格式化参数返回格式化后的字符串。
格式化参数规则和 标准C 规则基本相同,基本格式为 %字母
:
d
十进制o
八进制x
十六进制f
浮点数s
字符串
print(string.format("%.4f", 3.14159265))
-- 3.1415 | 保留 4 位小数
print(string.format("%d %x %o", 31, 31, 31))
-- 31 1f 37
d, m, y = 29, 3, 2023
print(string.format("%s %02d/%02d/%d", "today is:", d, m, y))
-- today is: 29/03/2023 | 输出2位十进制,并在前面补零
匹配字符串 find
string.find(原字符串, 待匹配串)
用来在原字符串中找到待匹配串,返回开始和结束的索引位置。
可以添加第三个参数限定匹配开始位置,表示从原字符串的哪个部分开始匹配,如 string.find(s, p, 2)
只从 s 的第二个字符开始匹配到结束。
只返回第一个匹配的位置。
添加第四个参数 false
(默认正则匹配) 或者 true
,当该参数为 true
时,匹配串被视为一个字符串,在非正则匹配中会有所区别。
print(string.find("abc abc", "ab"))
-- 1 2 | 开始为1 结束为2
print(string.find("abc abc", "ab", 2))
-- 5 6 | 开始为5 结束为6
替换字符 gsub
string.gsub(原字符s, 被替换的p, 替换的r, 替换次数)
将原字符串 s 中的子串 p 替换为 r,替换次数默认全部替换。
返回 2 个值:替换后的字符串,替换的次数
print(string.gsub("coffee", "f", "a"))
-- coaaee 2 | 输出替换后的字符串,以及替换了2次
print(string.gsub("coffee", "f", "a", 1))
-- coafee 1
同样的,可以用冒号简化语法,如 s:gsub("f", "a", 1)
。
也可以使用正则表达式:
local s = "abcd1234cccc"
print(s:gsub("%d", "x"))
-- abcdxxxxcccc 4 | 替换了 4 次
table库
table库 提供了通用的表处理函数,其存放在表 table
中,使用格式为 table.方法(参数)
。
table 并非元表,不能使用冒号语法糖,老老实实地写
table.方法(表, 参数)
吧。
连锁列出 concat
table.concat(表, "分隔符", 始, 末)
返回表中 始-末
位置的所有元素,用 分隔符 隔开。
后三个参数可选,默认为 table.concat(list, "", 1, #list)
。分隔符为空,列出全部表。
t = {"red", "yellow", "orange"}
print(table.concat(t, "; "))
-- red; yellow; orange
该方法只对数字下标起作用,字符串下标会报错。
插入元素 insert
table.insert(表, 下标, 元素)
在表的 下标
位置插入 元素
。
下标
参数可选,默认为 table.insert(list, #list, value)
在末尾插入元素。
移动元素 move
table.move(表1, 始, 末, 始2, 表2)
将自 始
到 末
下标的元素从 表1
移动到 表2
的 始2
向后。
表2
参数可选,默认为 table.move(list, i, j, k, list)
,即将 list表 的 i-j 元素移动到 k 开始的位置。k 可以在 i-j 之间。
创建新表 pack
table.pack(元素1, 元素2, ...)
创建一个数字索引的表并返回,其长度保存在 n
下标中。
t = table.pack(10, 20, 30, 40, 50)
-- t = {10, 20, 30, 40, 50, n=5}
移除元素 remove
table.remove(表, 索引)
将 索引
位置的值移除并返回,后面的元素向前补齐。
索引
参数可选,默认为 table.remove(list, #list)
移除末尾元素并返回。
排序 sort
table.sort(表)
将表从小到大(升序)排序。该排序原地进行,且不稳定,永久排序。
可以给其传入第二个函数参数,用该函数来决定排序的方法。该函数需要可以接收表内的任意两个元素作为参数,当第一个元素排在第二个元素之前时返回真。
返回值 unpack
table.unpack(表, 始, 末)
用来返回表中的 始-末
的元素。其可以用来切片,也可以用来返回某个元素。
索引参数可选,默认为 table.unpack(list, 1, #list)
返回整张表的值。
t = { 1, 4, 5, 6, 7, 32, 12, 76 }
print(t) -- 这个输出的是表的内存地址
print(table.unpack(t)) -- 输出表的值
-- 1 4 5 6 7 32 12 76
也可以直接在后面接表的形式 table.unpack{元素1, 元素2, ...}
a, b = table.unpack{2, 3, 7, 4}
print(a, b) -- 用前两个元素赋值,多余变量的会被忽略
-- 2 3
-- 也可以用 _ 的方式来略过前面的赋值
_, _, a = table.unpack{2, 3, 7, 4} -- 略过了前面两个值,直接赋了第三个
print(a)
-- 7
跨文件调用 require
在 A文件(A.lua
) 中引入 B文件(B.lua
),使用函数 require("B")
。B文件名不带后缀,称为接口。
若 B.lua
没有返回值,A.lua
在调用后会直接执行B中的内容,且不需要用变量接收返回值。
-- 文件 B.lua
print("Hello World!")
-- 文件 A.lua
require("B")
-- A.lua 输出结果
-- Hello World!
若 B.lua
存在文件返回,A.lua
中就可以保存返回值,让 变量 = require("接口")
。注意被调用的文件其他部分输出仍会自动进行。
-- B.lua
print("Hello World!")
return "done"
-- A.lua
local r = require("B")
print(r)
-- A.lua 结果
-- Hello World!
-- done
在有返回值的时候也可以不保存,编译器不会报错。
`require` 只运行一次
require
调用某个文件只会被运行一次,即便写很多行,上述代码也只会出现一个 Hello World! 结果。
多级文件调用
在工作文件区中平级,即 A.lua
和 B.lua
处于同一个文件夹中时,可以直接用文件接口调用。
当 A.lua
B.lua
C文件夹
处于同一层级,而 A.lua
需要调用 C文件夹
下的 C.lua
时,就存在多层级的文件调用,表示文件路径的分割符号为 .
,即接口名为 C文件夹.C
。
-- A.lua
local r2 = require("C文件夹.C") -- 调用 C文件夹 下的 C.lua 文件
自定义库函数
主逻辑文件为 A.lua
,需要在其中调用保存函数库文件 B.lua
。
B.lua
中编写使用的函数。这里用 table 保存,以方便用方法的形式调用。
local B = {}
-- 创建函数 B.say1
-- 函数 say1 在 table B 中保存
function B.say1()
print("1")
end
function B.say2()
print("2")
end
return B -- 返回 table B
在 A.lua
中调用。
-- 将 B.lua 中的 table B 保存在 A.lua 中的 B 中
-- 就可以用 B.xxx 的形式来调用编写在其中的函数方法
local B = require("B")
B.say1()
B.say2()
-- 1
-- 2
迭代器
迭代器多用来遍历 table,一般情况下是可以使用循环进行迭代的。
#长度迭代
循环的范围用 #table名
限定到 table 长度即可:
t = {"a", "b", "c", "d"}
for i=1,#t do
print(i,t[i])
end
-- 1 a
-- 2 b
-- 3 c
-- 4 d
ipairs 连续迭代
有专有的连续迭代器 键,值 in ipairs(table名)
来进行迭代:
for i,j in ipairs(t) do
print(i,j)
end
该结果与上面一样。
但当表中的索引不连续时,ipairs
迭代到空项就会停止。例子表 t
没有写键,默认键为连续数字,因此可以迭代完整。
当指定索引不连续,或者字符串键时,该迭代器就不再适用全部遍历:
t2 = {
[1] = "a",
[2] = "b",
[4] = "d"
}
for i,j in ipairs(t2) do
print(i, j)
end
-- 1 a
-- 2 b
-- 不会输出不连续后的
pairs 全部迭代
pairs()
迭代器会迭代全部的下标,和索引顺序以及字符索引无关。
t3 = {
apple = "a",
banana = "b",
clear = "c"
}
for i,j in pairs(t3) do
print(i, j)
end
-- apple a
-- banana b
-- clear c
for i,j in pairs(t2) do
prints(i, j)
end
-- 1 a
-- 2 b
-- 4 d
next 原理
pairs()
迭代器实际上调用了 next()
函数。该函数会不断调用其“自己认为”的下一个,直到调用完。所以实际上是可能乱序的。
内部作用机理如下:
t = {a=1, b=2, c=3, d=4}
next(t)
-- a 1
next(t,"a")
-- b 2
next(t,"b")
-- c 3
next(t,"c")
-- d 4
next(t,"d")
-- nil
注意,“自己认为”的第一项未必是写进去的第一项。
正则
正则表达式将同类的字符归类,表示一类的东西,如“字母”、“数字”等。
正则表达式规则如下:
- 具体字符表示本身,如 a、b、c、1、2、3……
%魔法字符
表示魔法字符本身(^$()%.[]*+-?).
表示任何字符%a
表示字母%l
小写字母%u
大写字母
%d
表示数字%w
数字和字母%c
表示控制字符%g
除空白符外可打印的字符%s
空白字符
%p
标点符号%x
16 进制数字符号[set]
表示 set 中所有字符的联合[%a%d]
所有字母和数字,和%w
相同[%a_]
所有字母 + 下划线[0-7]
8 进制数字[0-7%l%-]
8 进制数字 + 小写字母 + “-”符号[^set]
set 的补集
正则匹配
利用正则表达式规则可以对字符串内容进行筛选。
string.find()
返回首尾索引,string.match()
返回值为匹配到的子字符串。
若测试字符串 abcd1234cccc
,需要找到其中连续出现的 3 个数字:
local s = "abcd1234cccc"
string.find(s,"%d%d%d")
-- 5 7 | 匹配到的三个数字索引为 5-7
string.match(s,"%d%d%d")
-- 123 | 匹配到123
多条件匹配则用 []
框起表达式,其中是“或”的关系。如要找到后面有数字或者字母的 c
string.find(s, "c[%a%d]")
-- 3 4 | 匹配到 cd
string.match(s,"c[%a%d]")
-- cd
使用 [^正则表达式]
表示补集,如找到数字后不是数字的两个字符:
string.find(s, "%d[^%d]")
-- 8 9 | 匹配到 4c
string.match(s, "%d[^%d]")
-- 4c
使用 正则表达式*
表示尽量长的匹配(0个或多个),如找 d 和后面所有连续的数字:
string.find(s, "d%d*")
-- 4 8 | 匹配到 d1234
string.match(s, "d%d*")
-- d1234
使用 正则表达式+
也表示尽量长的匹配,但是是至少有 1 个,其与 *
的区别在如下:
-- 待匹配 abcd1234cccc
string.match(s, "c%d*")
-- c
string.match(s, "c%d+")
-- nil
使用 正则表达式-
表示尽量短的匹配,和其他正则表达式搭配使用,可以达到匹配头尾的效果,如需要找到头为 a 尾部为 c 中间只能是数字或没有的字符:
string.find(s, "d%d-c")
-- 4 9 | 匹配到 d1234c
string.match(s, "d%d-c")
-- d1234c
需要注意的是,匹配到的是第一个符合条件的字符串,即便后面出现了 d12c 这种里面数字更少的符合条件的子字符串也不会被匹配上。
()
则只对 string.match()
有影响,其只会返回括号内的东西,可以使用多个括号:
string.match(s, "d(%d+)")
-- 1234
string.find(s, "d(%d+)")
-- 4 8 | 仍返回全部的首尾索引
迭代正则匹配
string.match()
只会匹配第一个符合条件的子字符串,在循环中使用 string.gmatch()
迭代器可以匹配上所有的符合条件的字符串。
local s = "a1a2a3a4a5a6a7a8a9"
for w in string.gmatch(s, "a%d") do
print(w)
end
-- a1 a2 a3 a4 a5 a6 a7 a8 a9
-- 也可以写成
for w in s:gmatch("a%d") do
print(w)
end
元表与元方法
Lua 中的元表在形制上就是 table表,其用来存储元方法。
元方法是 Lua 本身定义好的,在某个功能无法执行时被尝试调用。如 a+b
加法在两端不是数字时,就会尝试调用元方法 __add
来看是否能正常运行。
元方法调用顺序机制为:按照变量出现的顺序,调用第一个可以使用的元方法,传入两个变量,执行 __add(a, b)
语句。
元方法必须定义在一个单独的元表中,再通过 setmetatable(目标表, 元表)
的方法为目标表注入元方法,让目标表可以使用。
举个例子
一个 table 表 t,直接执行 t+1
会报错,因为表无法执行 +
,且表 t 和数字 1 都没有定义元方法 __add
作为备选。
若此时定义一个元方法元表 mt,在其中更改 __add
键的值从而重新定义了该元方法。再通过 setmetatable(t, mt)
为 t 表注入元表 mt,使其的元方法 __add
发生改变,就可以正常执行 t+1
的语句。
local t = {a=1} -- 名为 t 的普通表
-- 定义 mt 表作元表
-- __add 是加法的元方法,在 mt 表中被重新定义
mt = {
__add = function(x, y)
return x.a+y -- 传入 x 的 a下标 索引对应的值 + y
end,
}
setmetatable(t, mt) -- 给 t 表注入带有元方法的元表 mt
print(t+1) -- 此时调用 mt 中的 __add 方法
-- 2
元方法表
__add
+ 操作__sub
- 操作__mul
* 操作__div
/ 操作__mod
% 操作__pow
^ 操作__unm
取负__idiv
向下取整除法
__band
& 按位与__bor
| 按位或__bxor
按位异或__bnot
按位非
__shl
<< 左移__shr
>> 右移
__concat
.. 连接__len
# 求长度__eq
== 相等__lt
< 小于__le
<= 小于等于
__index
索引不存在时(table不是表 或者 索引不存在)__newindex
索引赋值时
__call
调用非函数时
__index
__index
是一个比较特殊的元方法,其既可以是一个方法,也可以是一张表,保存备用的数据,形成一个类似于继承的效果。
local t = {} -- t 为空表
print(t["abc"]) -- nil
mt = {
__index = {
abc = 123,
def = 456
}
}
setmetatable(t, mt)
print(t["abc"]) -- 123
print(t["def"]) -- 456
定义 __index
为表时就可以存入多个数据,并且都可以在索引不存在时作为备用数据被访问查看并且调用,然而当 __index
为函数时,通常就只能返回一个键值对。
mt = {
__index = function(table, key)
return 123
end
}
__newindex
__newindex
元方法会在赋值时,且表键值不存在或者表不存在的时候触发。
该函数传入 3 个参数 __newindex = function(表, 键, 值)
t = {a = 1}
mt = {
__newindex = function(t,k,v)
-- 这里编写触发后的东西
end
}
setmetatable(t, mt)
需要注意的是,如果在 __newindex
中写插入新键值对的函数,则仍会因键不存在而再次调用该元方法,触发无尽循环,从而导致堆栈溢出。
如果一定要在这个元方法中编写新键值对的话,则需要使用 rawset(表, 键, 值)
函数,该函数会在不触发任何元方法的情况下把 表
的 键
设为 值
。
t = {a=5}
mt = {
__newindex = function(t, k, v)
rawset(t, k, v)
end
}
setmetatable(t, mt)
t["abc"] = 1
print(t["abc"])
-- 1
冒号语法糖
在 Lua 中,语法糖 v:name(args)
会被解释为 v.name(v, args)
,这里的 v 只会被求值一次。
面向对象
Lua 的面向对象功能是基于 table 表的特性,并结合语法糖使用。
如,给 t
添加属性 a
,并且让其有一个 add
的方法:
t = {
-- 初始属性 a
a = 10,
-- 方法 add
add = function(tab, x)
tab.a = tab.a + x
end
}
t:add(10) -- t.add(t, 10)
print(t["a"])
-- 20
在上面这个方法中,实现了面向对象的 属性
和 方法
两个要素。但是没有能够基于 类
来创建 实例
。
将一个表视为 类
,并创建一个类中的函数用来新建实例,再结合元方法,就可以实现面向对象的所有功能。
面向对象例子
- 对象类名
bag
- 构造函数
new
- 装入东西的方法
put
-- 创建一个空表作为 bag 类
bag = {}
-- 元表 bagmt
bagmt = {
put = function(x, item)
table.insert(x.items, item) -- 在 x(即bag.new中的临时表t) 的 items 属性中塞入传入的参数 item
end
}
-- 将 bagmt 的 __index 元方法设为其本身
-- 就可以在调用不存在的元素时,自动找自己的其他元素
bagmt["__index"] = bagmt
-- 用于构造实例的 new 方法
-- 命名为 bag.new,即写在 bag 表中
function bag.new()
local t = { -- t 为临时表,返回它来创建实例
items = {} -- items 是装入实例的元素
}
setmetatable(t, bagmt) -- 设置 bagmt 为 t 的元表,目的是要用 __index 元方法,在访问 t 不存在的下标时,去 bagmt 当中寻找
return t
end
在调用时的用法和逻辑如下:
-- 新建类 bag 的实例 b
-- 实例 b 是 bag.new 返回的临时表 t 决定的
-- 同样有 metatable bagmt
local b = bag.new()
-- 使用 put 方法
-- 因为 bagmt 元表的元方法是其本身
-- 而它自己是有 put 下标的,所以可以在临时表 t 中使用 put
-- 即可以在实例 b 中使用 put
b:put("apple")
-- "apple" 装在 b 的 items 下标里面。
for k,v in pairs(b.items) do
print(k, v)
end
-- 1 apple
在 实例b 调用 put
时,其 bag类 中并没有该方法,因此要找元表的 __index
下标来解决。
在创建 实例b 是用的是 bag类 中bag.new()
方法,其中设定了所有实例的元表为 bagmt,因此找 bagmt元表 的 __index
下标,尝试其中的解决方法。
bagmt元表 的 __index
下标设定为了其本身,因此视 bagmt表 为一个扩充(或继承)表,在其中找 put
下标。而其中有该下标,从而可以调用 put
函数方法。
接下来编写其他的几个函数方法。因为设定了 bagmt元表 在查询不到下标的解决方法 __index
是其本身,所以在 bagmt表 中写其他函数方法就可以了。
实现其他方法
- 拿出最后一个物品的方法
take
- 列出所有物品的方法
list
- 清空包包
clear
bagmt = {
-- put 方法,在表中的 items 下标中放进物品
put = function(t, item)
table.insert(t.items, item)
end,
-- take 方法,拿出最后一个物品
take = function(t)
return table.remove(t.items) -- 没有第二个参数,默认拿出最后一个
end,
-- list1 方法,用遍历的方式列出所有物品,输出分行
list1 = function(t)
for k,v in pairs(t.items) do
print(v)
end
end,
-- list2 方法,用连锁的方法列出所有物品,输出在同一行
list2 = function(t)
return table.concat(t.items, ", ")
end,
clear = function(t)
t.items = {} -- 直接设定为空表,就可以做到清空
end
}
几个解释:
bag
作为类,存数据。这个示例中数据存储在bag.items
中bagmt
用来存储 bag类 的方法,其实现是同时设定它为实例的元表和扩充表- 设定其扩充表为自身
bagmt["__index"] = bagmt
- 在构造实例时设定其为实例的元表
setmetatable(t, bagmt)
- 设定其扩充表为自身
bag.new()
是 bag类 中用来构造实例的函数。
协程
协程就是协同式多线程,Lua 实现协程的方式是对单线程进行分时复用,不过在宏观上仍然是多线程并行的——结果好一切都好!
关于协程的操作放在 coroutine库
中,该库同其他基础库的子库一样,都是一个独立的表。
创建协程 create/wrap
coroutine.create()
用 coroutine.create()
来创建协程,该接口创建的协程返回的变量类型为 thread.
一个基本的协程如下,新建了一个 co 的协程变量:
local co = coroutine.create(function()
print("Hello World!")
end)
co
相当于一个任务句柄,用来在之后对运行方式顺序等进行控制。需要配合其他函数,类似于 coroutine.resume(co)
来使用。
coroutine.resume(co)
-- Hello World!
执行继续运行的函数后,就会产生运行结果。
coroutine.wrap()
coroutine.wrap()
创建的协程返回值是一个函数,其不需要通过其他函数使用,直接调用这个返回值就可以运行该协程。
local co = coroutine.wrap(function()
print("Hello World!")
end)
co() -- 直接调用 co() 函数就可以运行
-- Hello World!
使用 wrap 创建协程相当于用返回值函数替代了 resume 函数,他们的所有逻辑都是一致的。
运行协程 resume
用 coroutine.resume()
开始/继续运行协程。
coroutine.resume(co) -- 开始运行 co 的协程
-- Hello World!
coroutine.resume()
函数的返回值是 boolean
类型,true 代表正常运行。
挂起协程 yield
coroutine.yield(传递给resume的参数)
用来挂起协程,写在协程里面。使用 coroutine.resume()
函数会暂停到 yield 这里,再次使用 resume 会从上一次暂停的地方继续运行。
local co = coroutine.create(function()
print("Hello World!")
coroutine.yield()
print("Hello World Again!")
end)
coroutine.resume(co)
-- Hello World! | 运行到 yield 前暂停
coroutine.resume(co)
-- Hello World Again! | 从上一次暂停的地方继续运行
向外交互
coroutine.yield(参数)
可以实现向协程外传输值,其中的参数会传递给 coroutine.resume()
函数的返回值。
local co = coroutine.create(function()
print("Hello World!")
coroutine.yield(1, 2, 3)
print("Hello World Again!")
end)
local a, b, c, d = coroutine.resume(co)
coroutine.resume(co)
print(b, c, d)
-- 1 2 3
coroutine.yield(参数)
中的参数会放在 coroutine.resume()
返回值的后面,其第一个返回值是用来判断协程式否正常运行 boolean 类型,而后的返回值就依次是 yield 传出的参数。
这个返回值构成为 true 参数1 参数2 ...
,且它并非是一个表,即在做 type(coroutine.resume())
时,得到结果永远是第一个返回值的类型 boolean
.
需要注意的是,只要出现 coroutine.resume()
函数,无论其返回值有没有被接收,协程都是开始/继续运行。
这是“内外交互”的一部分,yield 实现了从内部向外部传输数据。
向内交互
同样的,coroutine.resume(参数)
可以实现向内传输参数,参数会传输到正在挂起的 coroutine.yield()
的返回值中。
yield 的返回值并没有前面的 boolean 占据一个位置,所以可以直接接收。
local co = coroutine.create(function()
print("Hello World!")
local b1, b2 = coroutine.yield("传出1", "传出2", "传出3")
print("Hello World Again!", b1, b2)
end)
local _, a1, a2, a3 = coroutine.resume(co)
-- Hello World!
print(a1, a2, a3)
-- 传出1 传出2 传出3
coroutine.resume(co, "传入1", "传入2")
-- Hello World Again! 传入1 传入2
如果是采用 coroutine.wrap()
创建协程,在传入参数的时候用如下形式即可:
co("传入1", "传入2")
协程状态 status/running
coroutine.status(co)
会用字符串的形式返回协程 co 的状态:
- co 正在运行,返回
running
- co 挂起/未开始,返回
suspended
- co 是活动的,但不在运行,有其他协程运行,返回
normal
- co 已经运行完成/出错停止,返回
dead
一般来说,在协程内部检查状态的时候,会出现 running 或者 normal;而在外部检查状态时,总会是 suspended 或者 dead.
coroutine.running()
则会返回当前正在运行的协程和一个 boolean类型,在运行协程为主协程时为 true.
评论