抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

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 结束。支持 elseifelse

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 条件 doend 作为结构,其条件中定义的变量是作用域变量,只在该循环中生效。

条件格式为 变量 = 开始, 结束, 步长,其包含开始和结束的数字,即若为 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 条件 doend 构成。

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.luaB.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.

评论