在 Lua table 中我们可以访问对应的key来得到value值,但是却无法对两个 table 进行操作。
因此 Lua 提供了元表(Metatable),允许我们改变table的行为,每个行为关联了对应的元方法。
例如,使用元表我们可以定义Lua如何计算两个table的相加操作a+b。
当Lua试图对两个表进行相加时,先检查两者之一是否有元表,之后检查是否有一个叫__add
的字段,若找到,则调用对应的值。__add
等即时字段,其对应的值(往往是一个函数或是table)就是"元方法"。
有两个很重要的函数来处理元表:
以下实例演示了如何对指定的表设置元表:
mytable = {} -- 普通表
mymetatable = {} -- 元表
setmetatable(mytable,mymetatable) -- 把 mymetatable 设为 mytable 的元表
以上代码也可以直接写成一行:
mytable = setmetatable({},{})
以下为返回对象元表:
getmetatable(mytable) -- 这回返回mymetatable
以下为元表常用的字段:
算术类元方法: 字段:__add(+)
, __mul(*)
,__ sub(-)
,__div(/)
,__unm, __mod(%)
, __pow
, (__concat)
关系类元方法: 字段:__eq
,__lt(<)
, __le(<=)
,其他Lua自动转换 a~=b -- not(a == b)
a b -- b < a
a = b -- b <= a
(注意NaN的情况)
table访问的元方法: 字段: __index
, __newindex
__index: 查询:访问表中不存的字段&
rawget(t, i)
__newindex: 更新:向表中不存在索引赋值
rawset(t, k, v)
这是 metatable 最常用的键。
当你通过键来访问 table 的时候,如果这个键没有值,那么Lua就会寻找该table的metatable(假定有metatable)中的__index
键。如果__index
包含一个表格,Lua会在表格中查找相应的键。
$lua
Lua 5.4.1 Copyright (C) 1994-2020 Lua.org, PUC-Rio
other = {foo = 3}
t = setmetatable({},{__index = other})
t.foo
3
t.bar
nil
如果__index
包含一个函数的话,Lua就会调用那个函数,table和键会作为参数传递给函数。
__index
元方法查看表中元素是否存在,如果不存在,返回结果为 nil;如果存在则由 __index 返回结果。
mytable = setmetatable({key1 = "value1"}, {
__index = function(mytable, key)
if key == "key2" then
return "metatablevalue"
else
return nil
end
end
})
print(mytable.key1,mytable.key2)
-- value1 metatablevalue
实例解析:
__index
方法,如果__index
方法是一个函数,则调用该函数。我们可以将以上代码简单写成:
mytable = setmetatable({key1 = "value1"}, { __index = { key2 = "metatablevalue" } })
print(mytable.key1,mytable.key2)
__newindex
元方法用来对表更新,__index
则用来对表访问 。
当你给表的一个缺少的索引赋值,解释器就会查找__newindex
元方法:如果存在则调用这个函数而不进行赋值操作。
以下实例演示了 __newindex
元方法的应用:
mymetatable = {}
mytable = setmetatable({key1 = "value1"}, { __newindex = mymetatable })
print(mytable.key1)
mytable.newkey = "新值2"
print(mytable.newkey,mymetatable.newkey)
mytable.key1 = "新值1"
print(mytable.key1,mymetatable.newkey1)
以上实例执行输出结果为:
value1
nil 新值2
新值1 nil
以上实例中表设置了元方法__newindex
,在对新索引键(newkey)赋值时(mytable.newkey = “新值2”),会调用元方法,而不进行赋值。而如果对已存在的索引键(key1),则会进行赋值,而不调用元方法__newindex
。
以下实例使用了 rawset 函数来更新表:
mytable = setmetatable({key1 = "value1"}, {
__newindex = function(mytable, key, value)
rawset(mytable, key, "\""..value.."\"")
end
})
mytable.key1 = "new value"
mytable.key2 = 4
print(mytable.key1,mytable.key2)
-- new value "4"
以下实例演示了两表相加操作:
-- 计算表中最大Key值,table.maxn在Lua5.2以上版本中已无法使用
-- 自定义计算表中最大Key值函数 table_maxn
function table_maxn(t)
local mn = 0
for k, v in pairs(t) do
if mn < k then
mn = k
end
end
return mn
end
-- 两表相加操作
mytable = setmetatable({ 1, 2, 3 }, {
__add = function(mytable, newtable)
for i = 1, table_maxn(newtable) do
table.insert(mytable, table_maxn(mytable)+1,newtable[i])
end
return mytable
end
})
secondtable = {4,5,6}
mytable = mytable + secondtable
for k,v in ipairs(mytable) do
print(k,v)
end
以上实例执行输出结果为:
1 1
2 2
3 3
4 4
5 5
6 6
__add 键包含在元表中,并进行相加操作。 表中对应的操作列表如下:
模式 | 描述 |
---|---|
__add | 对应的运算符 ‘+’. |
__sub | 对应的运算符 ‘-’. |
__mul | 对应的运算符 ‘*’. |
__div | 对应的运算符 ‘/’. |
__mod | 对应的运算符 ‘%’. |
__unm | 对应的运算符 ‘-’. |
__concat | 对应的运算符 ‘…’. |
__eq | 对应的运算符 ‘==’. |
__lt | 对应的运算符 ‘<’. |
__le | 对应的运算符 ‘<=’. |
__call 元方法在 Lua 调用一个值时调用。以下实例演示了计算表中元素的和:
-- 计算表中最大Key值,table.maxn在Lua5.2以上版本中已无法使用
-- 自定义计算表中最大Key值函数 table_maxn
function table_maxn(t)
local mn = 0
for k, v in pairs(t) do
if mn < k then
mn = k
end
end
return mn
end
-- 定义元方法__call
mytable = setmetatable({10}, {
__call = function(mytable, newtable)
sum = 0
for i = 1, table_maxn(mytable) do
sum = sum + mytable[i]
end
for i = 1, table_maxn(newtable) do
sum = sum + newtable[i]
end
return sum
end
})
newtable = {10,20,30}
print(mytable(newtable))
-- 70
__tostring
元方法用于修改表的输出行为。以下实例我们自定义了表的输出内容:
mytable = setmetatable({ 10, 20, 30 }, {
__tostring = function(mytable)
sum = 0
for k, v in pairs(mytable) do
sum = sum + v
end
return "表所有元素的和为 " .. sum
end
})
print(mytable)
-- 表所有元素的和为 60
Lua 协同程序(coroutine)与线程比较类似:拥有独立的堆栈,独立的局部变量,独立的指令指针,同时又与其它协同程序共享全局变量和其它大部分东西。
协同是非常强大的功能,但是用起来也很复杂。
线程与协同程序的主要区别在于,一个具有多个线程的程序可以同时运行几个线程,而协同程序却需要彼此协作的运行。
在任一指定时刻只有一个协同程序在运行,并且这个正在运行的协同程序只有在明确的被要求挂起的时候才会被挂起。
协同程序有点类似同步的多线程,在等待同一个线程锁的几个线程有点类似协同。
方法 | 描述 |
---|---|
coroutine.create() | 创建coroutine,返回coroutine, 参数是一个函数,当和resume配合使用的时候就唤醒函数调用 |
coroutine.resume() | 重启coroutine,和create配合使用 |
coroutine.yield() | 挂起coroutine,将coroutine设置为挂起状态,这个和resume配合使用能有很多有用的效果 |
coroutine.status() | 查看coroutine的状态 注:coroutine的状态有三种:dead,suspend,running,具体什么时候有这样的状态请参考下面的程序 |
coroutine.wrap() | 创建coroutine,返回一个函数,一旦你调用这个函数,就进入coroutine,和create功能重复 |
coroutine.running() | 返回正在跑的coroutine,一个coroutine就是一个线程,当使用running的时候,就是返回一个corouting的线程号 |
以下实例演示了以上各个方法的用法:
-- coroutine_test.lua 文件
co = coroutine.create(
function(i)
print(i);
end
)
coroutine.resume(co, 1) -- 1
print(coroutine.status(co)) -- dead
print("----------")
co = coroutine.wrap(
function(i)
print(i);
end
)
co(1)
print("----------")
co2 = coroutine.create(
function()
for i=1,10 do
print(i)
if i == 3 then
print(coroutine.status(co2)) --running
print(coroutine.running()) --thread:XXXXXX
end
coroutine.yield()
end
end
)
coroutine.resume(co2) --1
coroutine.resume(co2) --2
coroutine.resume(co2) --3
print(coroutine.status(co2)) -- suspended
print(coroutine.running()) --nil
print("----------")
-- 结果
1
dead
----------
1
----------
1
2
3
running
thread: 0x7fb801c05868 false
suspended
thread: 0x7fb801c04c88 true
----------
coroutine.running就可以看出来,coroutine在底层实现就是一个线程。
当create一个coroutine的时候就是在新线程中注册了一个事件。
当使用resume触发事件的时候,create的coroutine函数就被执行了,当遇到yield的时候就代表挂起当前线程,等候再次resume触发事件。
接下来我们分析一个更详细的实例:
function foo (a)
print("foo 函数输出", a)
return coroutine.yield(2 * a) -- 返回 2*a 的值
end
co = coroutine.create(function (a , b)
print("第一次协同程序执行输出", a, b) -- co-body 1 10
local r = foo(a + 1)
print("第二次协同程序执行输出", r)
local r, s = coroutine.yield(a + b, a - b) -- a,b的值为第一次调用协同程序时传入
print("第三次协同程序执行输出", r, s)
return b, "结束协同程序" -- b的值为第二次调用协同程序时传入
end)
print("main", coroutine.resume(co, 1, 10)) -- true, 4
print("--分割线----")
print("main", coroutine.resume(co, "r")) -- true 11 -9
print("---分割线---")
print("main", coroutine.resume(co, "x", "y")) -- true 10 end
print("---分割线---")
print("main", coroutine.resume(co, "x", "y")) -- cannot resume dead coroutine
print("---分割线---")
-- 结果
第一次协同程序执行输出 1 10
foo 函数输出 2
main true 4
--分割线----
第二次协同程序执行输出 r
main true 11 -9
---分割线---
第三次协同程序执行输出 x y
main true 10 结束协同程序
---分割线---
main false cannot resume dead coroutine
---分割线---
以上实例接下如下:
resume和yield的配合强大之处在于,resume处于主程中,它将外部状态(数据)传入到协同程序内部;而yield则将内部的状态(数据)返回到主程中。
使用Lua的协同程序来完成生产者-消费者这一经典问题。
local newProductor
function productor()
local i = 0
while true do
i = i + 1
send(i) -- 将生产的物品发送给消费者
end
end
function consumer()
while true do
local i = receive() -- 从生产者那里得到物品
print(i)
end
end
function receive()
local status, value = coroutine.resume(newProductor)
return value
end
function send(x)
coroutine.yield(x) -- x表示需要发送的值,值返回以后,就挂起该协同程序
end
-- 启动程序
newProductor = coroutine.create(productor)
consumer()
以上实例执行输出结果为:
1
2
3
4
5
6
7
8
9
10
11
12
13
……
由于Lua语言强调可移植性和嵌入型,所以 Lua本身并没有提供太多与外部交互的机制 。在真实的Lua程序中,从图形、数据库到网络的访问等大多数I/O操作,要么由宿主机实现,要么通过不包括在发行版中的外部库实现
单就Lua语言而言,只提供了ISO C语言标准支持的功能,即基本的文件操作等
简单I/O模型虚拟了一个当前输入流和一个当前输出流,其I/O操作是通过这些流实现的
I/O库把当前输入流初始化为进程的标准输入(C语言中的stdin),将当前输出流初始化为进程的标准输出(C语言中的stdout)
简单I/O模型提供的接口有:io.input()、io.output()、io.write()、io.read()。
io.read()默认是从标准输入读取内容。
调用io.input()之后,程序后面的 所有输入都来自该函数指定的输入流 如io.input("test.txt")
将程序的输入流定向到test.txt文件中。
如果想要改变当前的输入流,再次调用io.input()即可。
io.write()默认将内容输出到标准输出中 。
调用io.output()之后,程序后面的 所有的内容都输出到该函数指定的输出流中 如io.output("test.txt")
将程序的输出流定向到test.txt文件中。
如果想要改变当前的输出流,再次调用io.output()即可。
该函数可以 将任意数量的字符串(或者数字)写入到输出流中 , io.write(args)是io.output():write(args)的简写 , 即函数write使用在当前输出流上。
格式:io.write(a, b, c...)
,所有内容会拼接在一起输出。
$ lua
Lua 5.4.1 Copyright (C) 1994-2020 Lua.org, PUC-Rio
io.write("sin(3) = ", math.sin(3), "\n")
sin(3) = 0.14112000805987
file (0x7ff4e600d760)
io.write(string.format("sin(3) = %.4f\n", math.sin(3)))
sin(3) = 0.1411
file (0x7ff4e600d760)
io.read() 可以从输入流中读取字符串 ,其参数决定了要读取的数据。
参数 | 描述 |
---|---|
"n" |
读取一个数字并返回它。例:file.read("n") |
"a" |
从当前位置读取整个文件。例:file.read("a") |
“L” | 读取下一行(保留换行符),在文件尾 (EOF) 处返回 nil。例:file.read("L") |
"l" (默认) |
读取下一行(丢弃换行符),在文件尾 (EOF) 处返回 nil。例:file.read("l") |
num |
返回一个指定字符个数num的字符串,或在 EOF 时返回 nil。例:file.read(5) |
io.read(0)是一个特例, 它常用于检测是否到达了文件末尾。如果仍然有数据可供读取,它会返回一个空字符串;否则,返回nil
io.read(args)实际上是io.input():read(args)的简写 ,即函数read使用在当前输入流上的
简单I/O模型对简单的需求而言还算适用,但对于诸如同时读写多个文件等更高级的文件操作来说就不够了。对于这些文件操作,我们需要用到完整I/O模型 。
io.open()用来打开一个文件,该函数仿造了C语言中的fopen()函数
file = io.open (filename [, mode])
mode | 描述 |
---|---|
r | 以只读方式打开文件,该文件必须存在。 |
w | 打开只写文件,若文件存在则文件长度清为0,即该文件内容会消失。若文件不存在则建立该文件。 |
a | 以附加的方式打开只写文件。若文件不存在,则会建立该文件,如果文件存在,写入的数据会被加到文件尾,即文件原先的内容会被保留。(EOF符保留) |
r+ | 以可读写方式打开文件,该文件必须存在。 |
w+ | 打开可读写文件,若文件存在则文件长度清为零,即该文件内容会消失。若文件不存在则建立该文件。 |
a+ | 与a类似,但此文件可读可写 |
b | 二进制模式,如果文件是二进制文件,可以加上b |
+ | 号表示对文件既可以读也可以写 |
返回值:
io.open("non-existent-file", "r")
nil non-existent-file: No such file or directory 2
local f = assert(io.open(filename, mode))
-- 打开流
local f = assert(io.open("filename", "r"))
-- 读取流中的所有内容
local t = f:read("a")
-- 关闭流
f:close()
io.stderr:write("Error", "\n")
-- 保存当前的输入流
local temp = io.input()
-- 打开一个新的输入流
io.input("newinput")
-- 对新的输入流进行一系列操作
-- 操作完成之后关闭输入流
io.input():close()
-- 恢复之前的输入流
io.input("temp")
for block in io.input()::lines(2^13) do
io.write(block)
end
该函数 用来获取和设置文件的当前位置 ,常常使用f:seek(whence, offset)的形式来调用
函数参数:
whence参数:
该参数是一个指定如何使用偏移的字符串,可以设置的值如下
offset参数: 根据参数whence,设置偏移值
返回值: 返回当前新位置在流中相对于文件开头的偏移
whence的默认值为"cur",offset的默认值为0。因此:
下面是一些演示案例:
function fsize(file)
local current = file:seek() -- 保存当前流偏移位置
local size = file:seek("end") -- 获取文件大小
file:seek("set", current) -- 恢复当前位置
return size
end
print(os.getenv("HOME"))
function createDir(dirname)
os.execute("mkdir" .. dirname)
end