理解 Lua 的那些坑爹特性
Lua 那些坑爹的特性
來源 https://blog.lilydjwg.me/2012/12/29/lua-caveats.36879.html協程只能在 Lua 代碼中使用
協程(coroutine)應該是 Lua 最大的賣點之一了??墒?#xff0c;它有一個在文檔中根本沒有提到過的弱點:只能在 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」錯誤。只有打過?Coco?補丁的版本才能正常執行。
| 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 中,提供了新的 API 來支持在 C 中 yield。不過,既然是 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 的那些坑爹特性
來源 ?http://sw.is-programmer.com/2013/1/3/understand-lua-caveats.36905.html?
按:最近看到了依云的文章,一方面,為Lua被人誤解而感到十分難過,另一方面,也為我的好友,依云沒有能夠體會到Lua的絕妙和優雅之處而感到很遺憾,因此我寫了這篇文章,逐條款地說明了依云理解中出現的一些問題。希望能夠幫助到大家!1. 協程只能在Lua代碼中使用
是的,協程在當你需要掛起一個C函數的時候無法使用。但是,在提出這個缺陷的時 候,是不是應該想一想:為什么Lua會有這個缺陷? 原因很簡單:這一點完全避不開,這是C的問題,C無法保存函數執行的某個現場用于 返回這個現場繼續執行,因此完全沒有辦法在Lua的協程被喚醒的時候,回到這個現場。 那么怎么辦呢?Lua5.2做出了很優秀的設計。既然無法回到C的現場,那么我們不回 去了,而是采取“事件通知”的方式告訴你,“hey哥們,你前面的邏輯被切了,想辦法 補救吧”,這就是所謂的CPS——繼續風格的編程。繼續在這里是一個Scheme/Lisp術 語,意思是“當前的操作執行完了以后,下面該做什么?”這種風格是Lua能支持任意 Yield 的必要條件。在C的限制下,只有這一種方法能突破這個限制。 至于你說的“比異步回調更復雜”,我想你弄混了兩點:1.這只是C API層面的修改 ,完全不影響到Lua代碼層面,你的Lua代碼完全不必做出任何修改,而且,你對 coroutine的用法完全錯了!等會兒我會教你coroutine到底怎么用。2.上面提到了, 這是唯一一種能支持coroutine的方式,既然是唯一一種,就無所謂復雜與否了。3. 我下面會演示給你,為什么說coroutine完全解放了程序員,使用coroutine的代碼會帶來 革命性的簡化。 我們分兩步來說明這個問題:第一步,我們先來看你的例子:你想做的事情是,在執 行 c.callback的時候,能夠yield出來,繼續其他的流程。這里必須要說明,你的API設 計本身就是callback式的,因此這種API本身就犯不著coroutine,Lua本身能完全地處理 。這里我會給出一個支持coroutine的C模塊設計,讓這個模塊能支持coroutine,第二步 ,我會告訴你coroutine實際上是用在什么方面的,是如何取代事件回調機制的。在完成 這個說明后,我們來說明coroutine到底有什么好處,為什么說coroutine比事件回調機制 有著?革命性的優秀之處。 你的例子是這樣的: ?| 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
我不否認,在我剛剛學習Lua的時候,我的確被nil坑過很多遍。我們先拋棄掉luaJIT 關于NULL設計的問題(這個設計本身也是一種無奈,而且LuaJIT畢竟并不能完全繼承Lua 作者對Lua的理念),先來看看nil究竟是什么——從nil中,我學習到了,在遇到坑爹特 性之前,先不要急著抱怨,想想為什么作者會設計這么坑爹的特性。要么作者是比你低能 的傻逼,要么這么設計就的確是有充分的考慮和不得已的苦衷的。這點你想到過嗎? nil是一個表示“沒有”的值。是的,就是真的“沒有”,因此nil本身就是一個幽靈 ——它除了表示“這里沒有東西”以外,沒有其他的任何含義!它不是None(None是一個 表示“空”的對象),它也不是NULL(NULL表示沒指向任何地方的指針——總所周知指針 本身必定是有值的,哪怕那個值是NULL)。Lua的作者十分聰明的將“沒有”這個概念也 引入了語言,并且還保持了語言的一致性:請問,將“沒有”存入一個表里面,它如果不 消失,還能發生什么事呢? 那么如何表示“空”或者“沒有指向任何地方的引用”呢?兩個辦法,你可以存入 false,或者可以用下面這個巧妙的方法: ?| 1 2 3 4 5 6 7 | undefined = {}????????????????????????????? -- 指定一個全局變量存在,但不指向任何地方: a = undefined?????????????????????????????? -- 判斷這個全局變量是否不指向任何地方:???? if a == undefine then ... end?????????????? -- 徹底刪除這個變量:?????????????????????? a = nil???????????????????????????????????? |
3. 沒有continue
是的,Lua一直不肯加入continue。為什么呢?因為repeat until。而為什么強調“ 不添加不必要的特性”的Lua作者會舍棄掉“那么常見”的continue,卻保留不那么常見 的repeat呢?是Lua的作者傻么? 不是。這是經過仔細設計的。我先告訴你答案,再仔細地分析:事實上,在加入 repeat以后,continue是邏輯上不可能實現的,而repeat語句實現了一個用其他的特性完 全無法取代的特性。 注意看repeat的結構: repeat <block> until <exp> 問題就在<exp>上了。Lua規定,<exp>是在<block>的作用域下計算的!這也就意味著 : ?| 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. 錯誤信息的表達
我只想說一句話:其實大多數在預見到會對錯誤進行處理的場合里面,錯誤的返回方 式其實并不是nil, errmsg,而是nil, errmsg, errno。別的你懂了。5. 下標
參看 novelties-5.2.pdf,說的非常明白了。6. 提前返回
這是一個語法問題,事實上return語句不跟著end的話,那么編譯器就根本無法編譯 return語句了。這是Lua“行無關語法”的一個必然折衷,我開始也不爽,但事實是,在 我數萬行的Lua開發中,除了測試必要要注釋一部分代碼以外,我根本沒用過do return end這種表達——至于為什么,你實際開發一下就知道了:因為這種代碼一定會導致完全 無法被執行到的死代碼。7. 方法調用
8. 面向對象
這兩點恰好就是Lua的優勢啊!!有時間我會寫一篇文章來討論。這實際上是Lua能以 比其他語言小巧靈活得多地去處理復雜邏輯的一個必然原因了。這里只說一點:Lua5.2中 ,表所具有的元方法已經和C API能處理的完全一樣多了。純Lua已經不必對著__len和 __gc而望洋興嘆了。 關于這一點,MikePall(LuaJIT實現者)還專門和Lua作者吵了一架,因為讓表支持 __gc會導致luaJIT的jit編譯非常難寫= =||||9. 結論
我開始學習Lua的時候,也幾乎得到了跟你一樣的結論。然而,在長達兩年的Lua開發 中,我逐漸認識到了Lua的美,認識到了Lua實現的優雅和嚴謹。現在如果有新手想學習C 語言開發的訣竅和技巧,我通常會建議他去拜讀Lua的C實現源代碼。Lua的實現太優雅了 。而Lua的設計也凝聚著作者的一點一滴的心血。Lua精準絕妙的設計是Lua強大的表達能 力的表現。繼續學習下去吧,我向你保證,你一定會發現,Lua實際上腳本語言里面表達 能力最強,概念最統一,設計最優雅的語言了。Lua無愧腳本語言之王! Category:?未分類?| Tags:?Lua?| Read Count: 36861 ?評論 (11) 1 [回復] 依云?說:?5 年前
關于協程,我當然知道協程該怎么用。Lua C API 確實有些細節上不太清楚,文檔太簡略了。pcallk 只能解決一次 yield 吧?如果 yield 的次數不定該怎么辦?我有個庫的函數,在運行過程中可能需要調用一個回調函數來取某些數據。在 Lua 綁定中,這個回調函數就是調用一個 Lua 函數,然后由于涉及網絡操作,它是會 yield 不定次數的。
你說的所有這些,要么是 LuaJIT 2.0.0 還沒實現的特性(LuaJIT 比 Lua 快太多了),要么是要求作者對 Lua 該怎么編程很熟悉(如果我接手的那些代碼是你寫的就好了)。至于表達能力,還是不要太強的好,不然每個人的錯誤返回方式和面向對象的實現都不一樣,概念是統一了,實現千差萬別、各不相容。
沒錯,「do return end」就是調試時用的。
2 [回復] 亞彌?說:?5 年前
@依云: 恩,說句實話,只看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唄……
5 年前
哇,偶像你也在這里!!
4 [回復] Larry Xu?說:?5 年前
newtry/protect???
luasocket中的那套,我當時看了也覺得蠻有意思的?
可否補充介紹下實際過程的使用方式
5 年前
你好,能給我lua郵件列表的郵箱嗎?謝謝~~
6 [回復] wtyqm 說:?4 年前
您好,想問下,如果想在pcall里使用coroutine,有什么辦法嘛? 文中的protect,指的就是luaSocket里那種封裝pcall的方式吧
7 [回復] 太陽神上?說:?4 年前
我覺得 lua 還有一個坑爹特性,table 當哈希表時,無法以 O(1) 的時間復雜度取得其元素個數。
8 [回復] 荒野無燈?說:?4 年前
從lily那里過來的。看了你這文章,受益頗多。
9 [回復] skyblue 說:?4 年前
求解釋coroutinize中這部分的必要性
local cur = current?
current = nil
3 年前
local a = true?
repeat a = false until a == false?
這樣吧
3 年前
關于continue的例子,我猜樓主的意思是這樣吧?
少寫了一個local?
local a = true?
repeat?
local a = false?
until a
?
總結
以上是生活随笔為你收集整理的理解 Lua 的那些坑爹特性的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 知乎上8个100K+高赞回答(筛选自63
- 下一篇: Objective-C:MRC(引用计数