原文鏈接:http://codemacro.com/2012/05/10/tolua-api/
我們使用tolua++手工綁定c/c++接口到lua中,在綁定的接口實(shí)現(xiàn)里,就需要取出傳入的參數(shù)。tolua++中提供了一系列tolua_toxxx函數(shù),例如:
lua_Number tolua_tonumber(lua_State *L, int narg, lua_Number def)
const char *tolua_tostring(lua_State *L, int narg, const char *def)
這些函數(shù)都有一個(gè)def參數(shù)。乍一看,這些函數(shù)使用起來(lái)很簡(jiǎn)單。傳入lua_State,傳入?yún)?shù)在棧中的位置,然后再傳一個(gè)失敗后返回的默認(rèn)值。
我重點(diǎn)要說(shuō)的是這里這個(gè)失敗,按正常程序員的理解,針對(duì)lua而言,什么情況下算失敗呢?lua語(yǔ)言里函數(shù)參數(shù)支持不傳,此時(shí)實(shí)參為nil,將nil轉(zhuǎn)換為一個(gè)c類型必然失敗;參數(shù)類型不正確算不算失敗?你傳一個(gè)user data,c里按數(shù)字來(lái)取,這也算失敗。
這么簡(jiǎn)單的API還需要多糾結(jié)什么呢?然后我們浩浩蕩蕩地寫(xiě)了上百個(gè)接口,什么tolua_tostring/tolua_tonumber的使用少說(shuō)也有500了吧?
然后有一天,服務(wù)器宕機(jī)了,空指針:
/* 失敗返回"",還能省空指針的判斷 */
const char *name = tolua_tostring(L, 1, "");
if (name[0] == '\0') { /* 空串總得判斷吧 */
...
}
跟蹤后發(fā)現(xiàn),腳本里傳入的是nil,這里的name取出來(lái)是NULL,而不是”“(的地址)。然后吐槽了一下這個(gè)API,辛苦地修改了所有類似代碼,增加對(duì)空指針的判斷。我沒(méi)有多想。
故事繼續(xù),有一天服務(wù)器雖然沒(méi)宕機(jī),但功能不正常了:
float angle = (float) tolua_tonumber(L, 1, 2 * PI);
...
這個(gè)意思是,這個(gè)函數(shù)的參數(shù)1默認(rèn)是2*PI,什么是默認(rèn)?lua里某函數(shù)參數(shù)不傳,或傳nil就是使用默認(rèn)。因?yàn)椴粋鞯脑挘@個(gè)實(shí)參本身就是nil。但,tolua_tonumber的行為不是這樣的,它的實(shí)現(xiàn)真是偷懶:
TOLUA_API lua_Number tolua_tonumber (lua_State* L, int narg, lua_Number def)
{
return lua_gettop(L)<abs(narg) ? def : lua_tonumber(L,narg);
}
TOLUA_API const char* tolua_tostring (lua_State* L, int narg, const char* def)
{
return lua_gettop(L)<abs(narg) ? def : lua_tostring(L,narg);
}
意思是,只有當(dāng)你不傳的時(shí)候,它才返回默認(rèn)值,否則就交給lua的API來(lái)管,而lua這些API是不支持應(yīng)用層的默認(rèn)參數(shù)的,對(duì)于lua_tonumber錯(cuò)誤時(shí)就返回0,lua_tostring錯(cuò)誤時(shí)就返回NULL。
這種其行為和其帶來(lái)的common sense不一致的API設(shè)計(jì),實(shí)在讓人蛋疼。什么是common sense呢?就像一個(gè)UI庫(kù)里的按鈕,我們都知道有click事件,hover事件,UI庫(kù)的文檔甚至都不需要解釋什么是click什么是hover,因?yàn)榇蠹铱吹竭@個(gè)東西,就有了共識(shí),無(wú)需廢話,這就是common sense。就像tolua的這些API,非常普通,大家一看都期待在意外情況下你能返回def值。但它竟然不是。實(shí)在不行,你可以模仿lua的check系列函數(shù)的實(shí)現(xiàn)嘛:
LUALIB_API lua_Number luaL_checknumber (lua_State *L, int narg) {
lua_Number d = lua_tonumber(L, narg);
if (d == 0 && !lua_isnumber(L, narg)) /* avoid extra test when d is not 0 */
tag_error(L, narg, LUA_TNUMBER);
return d;
}
即,根本不用去檢查棧問(wèn)題,直接在lua_tonumber之后再做包裝檢查。何況,lua需要你去檢查棧嗎?當(dāng)你訪問(wèn)了棧外的元素時(shí),lua會(huì)自動(dòng)返回一個(gè)全局常量luaO_nilobject:
static TValue *index2adr(lua_State *L, int idx) {
...
if (o >= L->top) return cast(TValue*, luaO_nilobject);
}
另,程序悲劇也來(lái)源于臆想。