协程(Coroutine)就是一段能自己中断和唤醒的程序片段,借助线程去理解,不同的是,它是由你来维护,看起来异步的逻辑,其实是同步顺序执行的,非常美妙。
接触过一段Unity开发,了解到C#里也有协程,但和之前Lua里的不太一样。C#只提供了一个yield关键字,借助原本就存在的枚举器(IEnumerator)实现的。yield return 将对象返回到枚举器的上层调用,同时保存枚举器的函数内状态,直到上层调用枚举器的下一个取值。本质上C#是为了简化代码,更容易实现一种枚举器的写法,而协程是附加产物。当然是可以简单封装一下,让协程的可中断特性浮现出来,Unity中的Coroutine,可以用于等待异步操作等,用起来很方便。
Lua的协程,提供了coroutine.yield和coroutine.resume,前者用于挂起,后者用于恢复执行,它们是可以相互传递参数的,就像一拉一推相互协助的两只手。那么问题来了,Lua能不能像C#那样玩枚举器?
(1)Lua的枚举器
没错就是for语句,是不是觉得写了这么多for ... ipairs/pairs,对for还是有些不太明白,总之有点复杂,反正pairs之类很好就够了?!
拿pairs分析一下:
pairs很简单,传入t一个table,返回next,t,nil。t还是那个t,关键在于next。单看next也很简单,next(t, k),t是一个table,k是某个table存在key,那么它返回key的下一个key和对应的value,初始状态是:next(t, nil)返回第一个key1,value1,最后一个next(t, keyn)返回nil。写这么多,这就是next啊,很准确的表达。
复杂的东西是由简单的元素构成的。
Lua文档上有这样的描述:
A for statement like
for var_1, ···, var_n in explist do block end
is equivalent to the code:
do local f, s, var = explist while true do local var_1, ···, var_n = f(s, var) var = var_1 if var == nil then break end block end end
用next放上去,pairs是这样干的:
k1,v1 = next(t,nil)
k2,v2 = next(t,k1)
...
nil = next(t, kn)
枚举的过程中,next的第1个参数始终是in 后面的第2个参数,第2个参数是变化的每次枚举返回的第1个参数,枚举过程直到返回nil为止。
还是有点晕,我们不看next的传入,它的返回参数每次都枚举出来的值,所以我们可以简单用一个有状态的函数来枚举,对用闭包,比如:
local t = {"A", "B", "C", "D"}
local i = 0
local myipairs = function()
i = i + 1
return t[i] and i, t[i]
end
for i, v in myipairs do
print(i, v)
end
for语句的in后面的参数就是枚举器,你可以用一个无状态的枚举器,当然需要紧跟后面一个不变量(通常是要枚举的容器),再跟一个初始可变量(通常是某种关键值)构成。当然也可以用有状态的函数闭包,因为函数有状态,所以每次使用需要一个工厂函数重新生成,上面的例子不能再使用一次,通常会改成这样:
local t = {"A", "B", "C", "D"}
function myipairs(t)
local i = 0
return function()
i = i + 1
return t[i] and i, t[i]
end
end
for i, v in myipairs(t) do
print(i, v)
end
function 里 return function,这很简单。
(2)用coroutine.yield实现枚举器
初步构思是想实现这样的用法:
function myipairs(t)
return function()
for i,v in ipairs(t) do
coroutine.yield(i, v)
end
end
end
local t = {"A", "B", "C", "D"}
function myipairs(t)
local co = coroutine.create(function()
for i,v in ipairs(t) do
coroutine.yield(i, v)
end
end)
return function()
local status, i, v = coroutine.resume(co)
if status then
return i, v
end
return nil
end
end
for i, v in myipairs(t) do
print(i, v)
end
它工作得很好。最后一个甜点:调用一次就会resume协程,同时如果成功就会返回resume的第2到最后参数,如果失败就会返回nil,Lua已经提供了这样的函数:coroutine.wrap,参看文档:
coroutine.wrap (f)
Creates a new coroutine, with body f
. f
must be a Lua function. Returns a function that resumes the coroutine each time it is called. Any arguments passed to the function behave as the extra arguments to resume
. Returns the same values returned by resume
, except the first boolean. In case of error, propagates the error
最终进一步简化上面的代码,最后一段代码展示枚举数组中任意2个值的组合,以此作为结束,一起感受Lua的简洁和美吧。
local t = {"A", "B", "C", "D"}
function myipairs(t)
return coroutine.wrap(function()
for i,v1 in ipairs(t) do
for j,v2 in ipairs(t) do
if i < j then
coroutine.yield(v1, v2)
end
end
end
end)
end
for a, b in myipairs(t) do
print(a, b)
end