Lua 那些坑爹的特性
协程只能在 Lua 代码中使用
协程(coroutine)应该是 Lua 最大的卖点之一了。可是,它有一个在文档中根本没有提到过的弱点:只能在 Lua 代码中使用,不能跨越 C 函数调用界限。也就是说,从 C 代码中无法直接或者间接地挂起一个在进入这个 C 函数之前已经创建的协程。而 Lua 本身作为一种易于嵌入的语言,必然不时与 C 打交道。
比如以下程序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | c = require ( 'c' ) co = coroutine.create ( function () print ( 'coroutine yielding' ) c.callback( function () coroutine.yield () end ) print ( 'coroutine resumed' ) end ) coroutine.resume (co) coroutine.resume (co) print ( 'the end' ) |
C 模块代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | #include<stdio.h> #include<stdlib.h> #include<lua.h> #include<lualib.h> #include<lauxlib.h> static int c_callback(lua_State *L){ int ret = lua_pcall(L, 0, 0, 0); if (ret){ fprintf (stderr, "Error: %s\n" , lua_tostring(L, -1)); lua_pop(L, 1); exit (1); } return 0; } static const luaL_Reg c[] = { { "callback" , c_callback}, {NULL, NULL} }; LUALIB_API int luaopen_c (lua_State *L) { luaL_register(L, "c" , c); return 1; } |
在官方版 Lua 以及 LuaJIT 中会出现「attempt to yield across metamethod/C-call boundary」错误。只有打过 补丁的版本才能正常执行。
1 2 3 4 5 6 7 8 9 10 | >>> lua5.1 co.lua coroutine yielding Error: attempt to yield across metamethod/C-call boundary >>> luacoco co.lua coroutine yielding coroutine resumed the end >>> luajit co.lua coroutine yielding Error: co.lua:6: attempt to yield across C-call boundary |
据说 LuaJIT 已经解决了这个问题,不过我想他们说的是内建函数支持 yield 而已。
在 Lua 5.2 中,。不过,既然是 C API,当然得改代码,而且看上去比异步回调更复杂。
幽灵一般的 nil
nil 相当于 Python 中的 None 或者 C 中的 NULL,表示「没有这个值」的意思。但是,一个神奇的地方在于,所有未定义的变量的值均为 nil。所以,在 Lua 中有空值 nil,但是有时它又不存在:当你尝试把 nil 值存到表里时,它会消失掉。
另外,当 nil 被传入接受可变参数的函数时,官方版 Lua 只能通过select('#', ...)
获取参数个数。至于 LuaJIT,很遗憾,没有办法。
LuaJIT 中还有这样一个值,它等于 nil。但是根据 Lua 语言标准,只有 false 和 nil 的值为假。于是,在 LuaJIT 中,两个相等的量,却有着不同的真值。它就是 ffi 中的 NULL 指针。
在另外一些地方,也会有其它各种库定义的 null 值,比如ngx.null
、cjson.null
。这些空值之间哪些相等哪些不等就难说了。
没有 continue
Lua 一直不肯添加 continue 关键字。作者声称不添加不必要的特性。请问有谁认为「repeat ... until」结构比「continue」关键字更有必要?于是,凡是本来应当使用 continue 的地方,都不得不弄一个大大的 if 语句:
1 2 3 4 5 | for line in configfile: if line.startswith( '#' ): contine parse_config(line) |
在 Lua 中只能这么写:
1 2 3 4 5 6 | for line in configfile do if string.sub (line, 1 , 1 ) == '#' then else parse_config(line) end end |
所以,Lua 代码的左边空白的形状都是些 45° 或者 135° 的斜线。
错误信息的表达
Lua 中,习惯的错误表达为,返回两个值,第一个为 nil 表示发生了错误,第二个为字符串,是错误信息。字符串形式的错误信息显示给用户挺不错的(想想微软喜欢的长长的错误号)。可是,程序里只好用模式匹配去判断是否发生了指定类型的错误。这多么像 VimScript 中的错误处理啊。journald 取代 syslog 的重要原因之一就是它存储的是结构化文本。Lua 错误处理最伟大的一点则是我们又回到了字符串匹配。别以为你可以返回一个 table 或者 userdata 来表达错误。很多库可不这么认为。当你的结构化错误被..
连接时你就会发现这厮没救了。
下标
别的编程语言下标都从 0 开始。Lua 为了更「人性化」,其下标从 1 开始。其实写多了也能习惯,除了当通过 ffi 获得一个 C 数组的时候……
提前返回
return 语句之后必须跟着一个end
。于是,很多提前返回的时候只能写do return end
。有意义么?
方法调用
访问表或者 userdata 的域使用一个点.
,连接字符串使用两个点..
。而方法定义和调用时,你需要垂直放置的两个点——冒号:
。它与域访问的一个点相比,也就多了四个像素,显示器不干净或者精神不佳的时候就得小心了!
面向对象
Lua 是不支持面向对象的。很多人用尽各种招术利用元表来模拟。可是,Lua 的发明者似乎不想看到这样的情形,因为他们把取长度的__len
方法以及析构函数__gc
留给了 C API。纯 Lua 只能望洋兴叹。
结论
Lua 只适合写写配置。做纯计算用用 LuaJIT 也不错。复杂的逻辑还是交给专业点的语言吧。
理解 Lua 的那些坑爹特性
1. 协程只能在Lua代码中使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | c = require ( 'c' ) co = coroutine.create ( function () print ( 'coroutine yielding' ) c.callback( function () coroutine.yield () end ) print ( 'coroutine resumed' ) end ) coroutine.resume (co) coroutine.resume (co) print ( 'the end' ) |
1 | local c = require 'c' |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | #include<stdio.h> #include<stdlib.h> #include<lua.h> #include<lualib.h> #include<lauxlib.h> static int c_callback(lua_State *L){ int ret = lua_pcall(L, 0, 0, 0); if (ret){ fprintf (stderr, "Error: %s\n" , lua_tostring(L, -1)); lua_pop(L, 1); exit (1); } return 0; } static const luaL_Reg c[] = { { "callback" , c_callback}, {NULL, NULL} }; LUALIB_API int luaopen_c (lua_State *L) { luaL_register(L, "c" , c); return 1; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | #include<stdio.h> #include<stdlib.h> #define LUA_LIB /* 告诉Lua,这是一个LIB文件 */ #include<lua.h> #include<lualib.h> #include<lauxlib.h> static int c_cont(lua_State *L) { /* 这里什么都不用做:因为你的原函数里面就没做什么 */ return 0; } static int c_callback(lua_State *L){ /* 使用 lua_pcallk,而不是lua_pcall */ int ret = lua_pcallk(L, 0, 0, 0, 0, c_cont); if (ret) { fprintf (stderr, "Error: %s\n" , lua_tostring(L, -1)); lua_pop(L, 1); exit (1); } /* 因为你这里什么都没做,所以c_cont里面才什么都没有。如果这里需要做 * 什么东西,将所有内容挪到c_cont里面去,然后在这里简单地调用 * return c_cont(L); * 即可。 */ return 0; } static const luaL_Reg c[] = { { "callback" , c_callback}, {NULL, NULL} }; LUALIB_API int luaopen_c (lua_State *L) { /* 使用新的 luaL_newlib 函数 */ luaL_newlib(L, c); return 1; } |
1 2 3 4 | lua -- co.lua coroutine yielding coroutine resumed the end |
1 2 3 4 5 6 7 8 9 | function do_login(server) server:login( function (data) -- 错误处理先不管,假设有一个全局处理错误的机制(后面会提到,实际 -- 上就是newtry/protect机制) server:get_player_info( function (data) player:move_to(data.x, data.y) end ) end , "username" , "password" ) end |
1 2 3 4 5 | function d_login(server) server:login( "username" , "password" ) local data = server:get_player_info() player:move_to(data.x, data.y) end |
1 2 3 4 5 6 7 8 9 10 11 | local current function server:login(name, password) assert ( not current, "already send login message!" ) server:callback_login( function (data) local cur = current current = nil coroutine.resume (cur, data) end , name, password) current = coroutine.running () coroutine.yield () end |
1 2 3 4 5 6 7 8 9 10 11 12 13 | function coroutinize(f, reenter_errmsg) local current return function (...) assert ( not current, reenter_errmsg) f( function (...) local cur = current current = nil coroutine.resume (cur, ...) end , ...) current = coroutine.running () coroutine.yield () end end |
2. 幽灵一般的 nil
1 2 3 4 5 6 7 | undefined = {} -- 指定一个全局变量存在,但不指向任何地方: a = undefined -- 判断这个全局变量是否不指向任何地方: if a == undefine then ... end -- 彻底删除这个变量: a = nil |
3. 没有continue
1 2 | local a = true repeat a = false until a |
1 2 3 4 5 6 7 | for line in configfile do if string.sub (line, 1 , 1 ) == '#' then goto next end parse_config(line) :: next :: end |
1 2 3 4 5 6 7 8 9 10 | local i repeat if i == 5 then goto next end local j = i * i print ( "i = " ..i.. ", j = " ..j) i = i + 1 :: next :: until i == 10 |
1 2 3 4 | lua -- "noname\2013-01-03-1.lua" lua: noname\2013-01-03-1.lua:10: <goto next> at line 4 jumps into the scope of local 'j' shell returned 1 Hit any key to close this window... |
4. 错误信息的表达
5. 下标
6. 提前返回
7. 方法调用
8. 面向对象
9. 结论
关于协程,我当然知道协程该怎么用。Lua C API 确实有些细节上不太清楚,文档太简略了。pcallk 只能解决一次 yield 吧?如果 yield 的次数不定该怎么办?我有个库的函数,在运行过程中可能需要调用一个回调函数来取某些数据。在 Lua 绑定中,这个回调函数就是调用一个 Lua 函数,然后由于涉及网络操作,它是会 yield 不定次数的。
你说的所有这些,要么是 LuaJIT 2.0.0 还没实现的特性(LuaJIT 比 Lua 快太多了),要么是要求作者对 Lua 该怎么编程很熟悉(如果我接手的那些代码是你写的就好了)。至于表达能力,还是不要太强的好,不然每个人的错误返回方式和面向对象的实现都不一样,概念是统一了,实现千差万别、各不相容。
没错,「do return end」就是调试时用的。
@依云: 恩,说句实话,只看reference的确很难搞明白k系列函数内部的核心思想。我是一开始就跟着邮件列表的讨论才比较清楚的。不过你真的可以看看novelties-5.2.pdf,这里面有很详细的说明。
另外不明白“一次yield”和“多次yield”有什么区别。只要用了k系列函数,你多少yield都没问题的,因为Lua自己会帮你维护Lua内部yield时候的状态。无论你如何yield,回到C层面(即从内部的coroutine返回)只会有一次,因此k系列函数一定能做到你想要的,而且并不需要特别的设计。 你仔细看看LuaJIT,很多特性已经实现了,包括goto。k系列函数没实现是基于两个原因:1.LuaJIT关注纯Lua应用,甚至用ffi库取代了C API的必要性;2.LuaJIT因为与Lua作者的巨大分歧(邮件里面吵了好几架),所以不打算实现5.2兼容了。至少短期内是不想的。sigh……快的话,其实快不了多少,只是科学计算方面的确快了很多,如果你的代码是C模块密集的,那么LuaJIT很难提高效率,其次是如果你用了NYI的特性,那么也是不会快的(比如字符串模式匹配和coroutine),从我的经验看,网游逻辑书写用luaJIT对效率的提升不大,甚至可能比原Lua更慢。 表达能力问题的确是个双刃剑,但有个朋友说得好“做得到总比做不到好”,这个就看怎么解读了。 错误返回是有标准模式的,文章里面提到了newtry/protect模式,不过写到后来写忘了= =有时间补上吧,OO的话也是有标准模式的,而且是两套。关键是,因为底层概念统一,所以即使是千差万别的实现,最终也一定是兼容的。你如果处理过实现之间的纠葛就会体会到底层概念统一带来的巨大好处。 do return end的话也就是一个词和三个词的区别吧……sigh……就当多打字了,实在不行做个imap或者iab呗……哇,偶像你也在这里!!
newtry/protect??
luasocket中的那套,我当时看了也觉得蛮有意思的 可否补充介绍下实际过程的使用方式你好,能给我lua邮件列表的邮箱吗?谢谢~~
您好,想问下,如果想在pcall里使用coroutine,有什么办法嘛? 文中的protect,指的就是luaSocket里那种封装pcall的方式吧
我觉得 lua 还有一个坑爹特性,table 当哈希表时,无法以 O(1) 的时间复杂度取得其元素个数。
从lily那里过来的。看了你这文章,受益颇多。
求解释coroutinize中这部分的必要性
local cur = current
current = nillocal a = true
repeat a = false until a == false 这样吧关于continue的例子,我猜楼主的意思是这样吧?
少写了一个local?
local a = true
repeat local a = false until a