Lua进阶

🤔


Lua协程

  • 协程类似于线程,是一条执行序列,拥有自己独立的栈、局部变量和指令指针,同时与其他协程共享全局变量等。
  • 线程与协同程序的主要区别在于,一个具有多个线程的程序可以同时运行几个线程,而协同程序却需要彼此协作的运行。就是说,一个具有多个协程的程序在任意时刻只能运行一个协程,并且正在运行的协程只有在其显式的要求挂起时,他的执行才会暂停。
  • Lua将所有协程相关的函数放在一个名为“coroutine”的table中,其中主要方法有:
方法 描述
create() 用于创建新的协程,返回一个协程类型的值,参数是一个函数
resume() 用于启动或重启一个协程
yield() 挂起一个协程
wrap() 创建一个新协程,返回一个函数,调用函数时开启协程
status() 查看协程状态

通过status可以查看到一个协程可以处于4种不同的状态,分别是挂起(suspended)、运行(running)、死亡(dead)、正常(normal),当创建一个协程时,它处于挂起状态:

1
2
3
4
5
6
7
co = coroutine.create(
function ()
print('Hello, World')
end
)

print(coroutine.status(co))

输出:suspended --协程处于挂起状态

通过resume启动这个协程:

1
2
coroutine.resume(co)
print(coroutine.status(co))

输出:

1
2
Hello, World
dead -- 处于死亡状态

协程真正强大之处在于yield函数,该函数可以使一个运行中的协程处于挂起,而后再恢复他的运行:

1
2
3
4
5
6
7
8
9
co = coroutine.create(
function ()
for i = 1, 10 do
print(i)
coroutine.yield()
end
end)

coroutine.resume(co)

输出:

1
2
1
suspended --处于挂起状态

在这里,当唤醒协程时它开始执行,直到遇到yield,协程会被挂起,此时的活动都发生在yield调用中,当恢复协程执行,对于yield的调用才会最终返回,然后写成继续执行,直到下一个yield或执行结束:

1
2
3
for i = 1, 10 do
coroutine.resume(co)
end

输出:2 3 4 5 6 7 8 9 10
再最后一次调用时,协程的内容已经执行完毕,并已经返回。因此这是的协程处于死亡状态,没有输出,如果试图再次执行它,resume将返回false及一条错误信息:print(coroutine.resume(co))
输出:false cannot resume dead coroutine

下面是一个综合的实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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
2
3
4
5
6
7
8
9
10
11
12
第一次协同程序执行输出     1     10
foo 函数输出 2
main true 4
--分割线----
第二次协同程序执行输出 r
main true 11 -9
---分割线---
第三次协同程序执行输出 x y
main true 10 结束协同程序
---分割线---
main false cannot resume dead coroutine
---分割线---


Lua元表和元方法

  • 通常Lua中的每个值都有一套预定义的操作集合。例如数字四则运算,字符串链接,增加table的键值对等。但无法将两个table相加,无法对函数做比较,也无法调用字符串。
  • 我们可以通过元表来修改一个值的行为,使其在面对一个非预定义的操作时执行一个指定的操作。如上所诉,可以通过元表让两个table相加。当Lua试图将两个table相加时,会先检测两者之一是否有元表,然后检测该元表中是否有__add字段,若找到该字段,就调用该字段对应的值。这个值也就是所谓的元方法(通常是一个函数或者table),这个函数会用于计算两个table的和。
  • Lua中的每一个值都有一个元表,table和userdata可以有各自独立的元表,其他类型则共享其类型所属的单一元表。Lua在创建新的table时不会创建元表。

    Lua中有两个重要的函数处理元表:

  • setmetatable(table, metatable): 对指定table设置元表(metatable),如果元表(metatable)中存在__metatable键值,setmetatable会失败 。
  • getmetatable(table): 返回对象的元表(metatable)。
1
2
3
t = {}
t1 = {}
setmetatable(t, t1)

上面的代码可以简化为一行:
t = setmetatable({}, {})

  • 任何table都可以作为任何值的元表,而一组相关的table也可以共享一个通用的元表,此元表描述了他们的共同行为。一个table甚至可以作为其自己的元表,用于描述其特有的行为。
  • 在Lua代码中只能设置table的元表,若要设置其他类型值的元表,则必须通过c代码来完成。

__index元方法

  • 当访问一个table不存在的字段时,会返回nil。这些访问会促使解释器去查找一个叫做__index的元方法,如果没有这个方法,那么访问结果就会为nil,否则就由这个元方法来提供结果。

假设要创建一个窗口的table,table中需要有位置、大小、背景等参数,所有参数都有默认值,因此希望在创建窗口对象时可以仅指定除了默认值的其他参数,这里我们可以这样实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
window = {} --命名空间
window.prototype = {x = 0, y = 0, width = 70, height = 70} --原型窗口
window.mt = {} --创建元表
function window.new(o) --声明构造函数
setmetatable(o, window.mt)
return o
end

window.mt.__index = function(table, key) --定义元方法
return window.prototype[key]
end

w = window.new{x = 10, y = 20} -- 创建一个新窗口
print(w.width)--查询它没有的字段
--这里输出 70

__newindex元方法

  • newindex元方法和index类似,不同之处在于前者用于table更新,而后者用于在table中查询,当对一个table中不存在的索引赋值时,解释器就会查找这个元方法,如果有,解释器就调用它,而不是执行赋值。如果这个元方法是一个table,解释器就在这个table中赋值,而不是对原来的table。例:
1
2
3
4
5
6
7
8
9
10
t1 = {}
t = setmetatable({key1 = "value1"}, { __newindex = t1 })

print(t.key1)

t.newkey = "新值2"
print(t.newkey,t1.newkey)

t.key1 = "新值1"
print(t.key1,t1.key1)

输出:

1
2
3
value1
nil 新值2
新值1 nil

算数类元方法

  • Lua中每种算数操作符都有对应的字段名,如下所示:
字段名 描述
__add 对应运算符”+”
__sub 对应运算符”-”
__mul 对应运算符”*”
__div 对应运算符”/”
__unm 对应运算符”-”
__concat 对应运算符”..”
__eq 对应运算符”==”
__mod 对应运算符”%”
__pow 对应运算符”^”
__lt 对应运算符”<”

__tostring元方法

  • 函数print总是调用tostring来格式化输出。当格式化任意值时,tosrting会检查该值是否有__tostring元方法,如果有,tostring就会用该值作为参数来调用这个元方法。
1
2
3
4
5
6
7
8
9
10
t = setmetatable({ 10, 20, 30 }, {
__tostring = function(t)
sum = 0
for k, v in pairs(t) do
sum = sum + v
end
return "表所有元素的和为 " .. sum
end
})
print(t)

输出:表所有元素的和为 60

宇 wechat
扫描二维码,订阅微信公众号