在我發表《Lua中的類型與值》這篇文章時,就有讀者給我留言了,說:你應該好好總結一下Lua中的function和userdata類型。如今是時候總結了。對於function,我在《Lua中的函數》這篇文章中進行了總結,而這篇文章將會對Lua中的userdata進行仔細的總結。對於文章,你們若是有任何疑議,均可以在文章的下方給我留言,也能夠關注個人新浪微博與我互動。學習,就要分享,我期待你的加入。html
userdata是啥?簡單直譯就是用戶數據,若是再文藝一點,就叫作用戶自定義數據。要這貨有什麼好處呢?首先,讓咱們來想象一個場景,你能夠在C中定義struct,當你在C中定義了一個struct,你有麼有想過,如何讓Lua表示這個struct,也就是說,Lua和C要進行溝通,如何讓Lua也能正確的訪問這個struct呢?這是一個符合實際且實用的需求。遇到這種需求,怎麼辦?這個時候,實用userdata就能大展身手了,所以Lua爲此提供了一種基本的類型——userdata。userdata提供了一塊原始的內存區域,能夠用來存儲任何東西。而且,在Lua中userdata沒有任何預約義的操做。先來看看怎麼使用userdata。編程
函數lua_newuserdata會根據指定的大小分配一塊內存,並將對應的userdata壓入棧中,最後返回這個內存塊的地址:數組
void *lua_newuserdata(lua_State *L, size_t size);
下面,就經過一簡單的實例來講說userdata的使用。函數
static struct StudentTag { char *strName; // 學生姓名 char *strNum; // 學號 int iSex; // 學生性別 int iAge; // 學生年齡 };
定義一個學生結構體,以後的操做,都在這個學生結構體上進行,包括設置學生姓名,學號,性別和年齡。學習
static int Student(lua_State *L) { size_t iBytes = sizeof(struct StudentTag); struct StudentTag *pStudent; pStudent = (struct StudentTag *)lua_newuserdata(L, iBytes); return 1; // 新的userdata已經在棧上了 }
建立一個新的學生結構體,使用的lua_newuserdata函數,建立完成之後,這個新的userdata就在棧上,能夠直接返回給Lua。下面就以設置姓名和獲取姓名爲例子。ui
static int GetName(lua_State *L) { struct StudentTag *pStudent = (struct StudentTag *)lua_touserdata(L, 1); luaL_argcheck(L, pStudent != NULL, 1, "Wrong Parameter"); lua_pushstring(L, pStudent->strName); return 1; } static int SetName(lua_State *L) { // 第一個參數是userdata struct StudentTag *pStudent = (struct StudentTag *)lua_touserdata(L, 1); luaL_argcheck(L, pStudent != NULL, 1, "Wrong Parameter"); // 第二個參數是一個字符串 const char *pName = luaL_checkstring(L, 2); luaL_argcheck(L, pName != NULL && pName != "", 2, "Wrong Parameter"); pStudent->strName = pName; return 0; }
在GetName函數中,只有一個參數,那就是使用Student函數建立的userdata,而後使用C語言的方式,從中取出名字,放到棧中,返回到Lua中。lua
在SetName函數中,須要傳入兩個參數,第一個參數是userdata,第二個參數是須要設置的值,而後直接賦值就行了,使用起來比較簡單,沒有很複雜的步驟,你覺的呢?spa
上述的代碼有一個很嚴重的問題,爲何這麼說呢?我先把上一個例子的Lua代碼貼出來:指針
require "userdatademo1" local objStudent = Student.new() Student.setName(objStudent, "果凍想") Student.setAge(objStudent, 15) local strName = Student.getName(objStudent) local iAge = Student.getAge(objStudent) print(strName) print(iAge)
調用Student的new獲得一個Student實例之後,之後調用Student的其它函數時,第一個參數都是使用Student函數獲得的userdata,也就是上面代碼中的objStudent。在C模塊側,咱們只是簡單的判斷了一下傳進來的userdata是否爲NULL,並無辦法判斷傳進來的userdata參數是使用Student函數獲得的;若是我傳一個錯誤的userdata進去,程序也會繼續運行,但有可能使內存遭到破壞。那如何肯定咱們傳入的userdata正是咱們須要的userdata呢?咱們須要一種這樣的機制來確保參數的合法性。code
一種辨別不一樣類型的userdata的方法是,爲每種類型建立一個惟一的元表(什麼是元表?)。每當建立了一個userdata後,就用相應的元表來標記它。而每當獲得一個userdata後,就檢查它是否擁有正確的元表。因爲Lua代碼不能改變userdata的元表,所以也就沒法欺騙代碼了。
爲每一個userdata都建立一個元表,那就須要有個地方來存儲這個新的元表。在Lua中,一般習慣是將全部新的C類型註冊到註冊表中,以一個類型名做爲key,元表做爲value。因爲註冊表中還有其它的內容,因此必須當心地選擇類型名,以免與key衝突。
Lua的輔助庫中提供了一些函數來幫助實現上面說的內容,可使用的輔助庫函數有:
int luaL_newmetatable(lua_State *L, const char *tname); void luaL_getmetatable(lua_State *L, const char *tname); void *luaL_checkudata(lua_State *L, int index, const char *tname);
luaL_newmetatable函數會建立一個新的table用做元表,並將其壓入棧頂,而後將這個table與註冊表中的指定名稱關聯起來。luaL_getmetatable函數能夠在註冊表中檢索與tname關聯的元表。luaL_checkudata能夠檢查棧中指定位置上是否爲一個userdata,而且是否具備與給定名稱相匹配的元表,若是該對象不是一個userdata,或者它不具備正確的元表,就會引起一個錯誤;不然它就會返回這個userdata的地址。如今來重寫上面的那個例子:
int luaopen_userdatademo2(lua_State *L) { // 建立一個新的元表 luaL_newmetatable(L, "Student"); luaL_register(L, "Student", arrayFunc); return 1; }
建立一個新元表,做爲該userdata的惟一標識。
static int Student(lua_State *L) { size_t iBytes = sizeof(struct StudentTag); struct StudentTag *pStudent; pStudent = (struct StudentTag *)lua_newuserdata(L, iBytes); // 設置元表 luaL_getmetatable(L, "Student"); lua_setmetatable(L, -2); return 1; // 新的userdata已經在棧上了 }
在建立userdata的時候,設置該userdata的元表。在使用的時候,咱們就能夠調用luaL_checkudata對參數進行檢查。
static int GetName(lua_State *L) { struct StudentTag *pStudent = (struct StudentTag *)luaL_checkudata(L, 1, "Student"); lua_pushstring(L, pStudent->strName); return 1; }
當寫下一下Lua語句時:
Student.getAge(io.stdin)
就會拋出這樣的異常錯誤:
bad argument #1 to 'getAge' (Student expected, got userdata)
如今,我想你應該懂得了如何去簡單的使用userdata了吧。接下來,上點難的東西。單擊這裏下載完整項目工程userdatademo2.zip。
關於Lua的面向對象對象編程,我在《Lua中的面向對象編程》這篇文章中進行了總結,若是你對Lua中的面向對象編程還不是很熟悉,能夠再去閱讀一下《Lua中的面向對象編程》。
在上面的Lua代碼中,能夠看到,我都是使用如下方式調用函數的:
local strName = Student.getName(objStudent)
這種調用方式無可厚非,可是從面向對象的角度來講,我new了一個對象,這就是一個獨立的對象,我應該這樣調用,才能更好理解啊。
local strName = objStudent:getName()
是吧。這又回到了《Lua中的面向對象編程》一文中說到的問題,因爲getName、setName等這些函數都是在Student中定義的,而在objStudent對象中,並無這些函數的定義,怎麼辦?仍是老辦法,咱們須要設置objStudent的元表,設置__index字段,當在objStudent中找不到對應的函數時,就去Student中查找,以前在《Lua中的面向對象編程》一文中,也是介紹的這個辦法。來吧,實現一下吧。
int luaopen_userdatademo3(lua_State *L) { // 建立一個新的元表 luaL_newmetatable(L, "Student_Metatable"); // 元表.__index = 元表 lua_pushvalue(L, -1); lua_setfield(L, -2, "__index"); luaL_register(L, NULL, arrayFunc_meta); luaL_register(L, "Student", arrayFunc); return 1; }
上述代碼中,有兩個地方要特別注意。首先要設置Student.__index = Student,使用上面代碼中的第七、8行實現的。還有一個須要注意的地方是luaL_register的特殊用法。在第一次調用luaL_register時,它的第二個參數是NULL,這樣的話,luaL_register不會建立任何用於存儲函數的table,而是以棧頂的table做爲存儲函數的table,而如今棧頂的table就是luaL_newmetatable建立的元表。代碼中,兩個luaL_register的第三個參數是不同的,它們的定義以下:
static struct luaL_reg arrayFunc[] = { { "new", Student }, { NULL, NULL } }; static struct luaL_reg arrayFunc_meta[] = { { "getName", GetName }, { "setName", SetName }, { "getAge", GetAge }, { "setAge", SetAge }, { "getSex", GetSex }, { "setSex", SetSex }, { "getNum", GetNum }, { "setNum", SetNum }, {NULL, NULL} };
最終調用luaL_register(L, 「Student」, arrayFunc);獲得的Student表中,就只有一個函數new;而元表Student_Metatable中則有arrayFunc_meta數組中包含的全部方法。我在把Lua代碼貼上來,而後再詳細的分析一下流程。
require "userdatademo3" local objStudent = Student.new() objStudent:setName("果凍想") objStudent:setAge(15) local strName = objStudent:getName() local iAge = objStudent:getAge() print(strName) print(iAge)
固然了,除了__index能夠從新被定義之外,其它預約義的元方法也能夠被從新定義。在完整的項目工程中提供了__tostring元方法的實現,單擊這裏下載完整工程userdatademo3.zip。
怎麼又有了一個輕量級userdata了?這貨又是什麼?專業點,叫作「light userdata」。我在前面總結的userdata叫作「full userdata」。
輕量級userdata是一種表示C指針的值(即void *)。因爲它是一個值,因此不用建立它。要將一個輕量級userdata放入棧中,只須要調用lua_pushlightuserdata便可。
void lua_pushlightuserdata(lua_State *L, void *p);
儘管兩種userdata在名稱上差很少,但它們之間仍是存在很大不一樣的。輕量級userdata不是緩衝,只是一個指針而已。它也沒有元表,就像數字同樣,輕量級userdata不受到垃圾收集器的管理。
輕量級userdata的真正用途是相等性判斷。一個徹底userdata是一個對象,它只與自身相等。而一個輕量級userdata則表示了一個C指針的值。所以,它與全部表示同一個指針的輕量級userdata相等。能夠將輕量級userdata用於查找Lua中的C對象。
如今就來講一種輕量級userdata的使用,還記的我在《再說C模塊的編寫(2)》中總結的註冊表麼?談及註冊表的key的時候,說使用UUID是一種不錯的方案,如今就使用輕量級的userdata結合static來實現無衝突的key。
// 壓入輕量級userdata,一個static變量的地址 static char key = 'k'; lua_pushlightuserdata(L, (void *)&key); lua_pushstring(L, "JellyThink"); lua_settable(L, LUA_REGISTRYINDEX);
因爲靜態變量的地址在一個進程中具備惟一性,因此絕對不會出現重複key的問題。
// 從註冊表中取對應的值 lua_pushlightuserdata(L, (void *)&key); lua_gettable(L, LUA_REGISTRYINDEX);