28.3 访问面向对象的数据 |
下面我们来看看如何定义类型为对象的userdata,以致我们就可以使用面向对象的语法来操作对象的实例,比如:
a = array.new(1000)
print(a:size()) --> 1000
a:set(10, 3.4)
print(a:get(10)) --> 3.4
记住a:size()等价于 a.size(a)。所以,我们必须使得表达式a.size调用我们的getsize函数。这儿的关键在于__index 元方法(metamethod)的使用。对于表来说,不管什么时候只要找不到给定的key,这个元方法就会被调用。对于userdata来讲,每次被访问的时候元方法都会被调用,因为userdata根本就没有任何key。
假如我们运行下面的代码:
local metaarray = getmetatable(array.new(1))
metaarray.__index = metaarray
metaarray.set = array.set
metaarray.get = array.get
metaarray.size = array.size
第一行,我们仅仅创建一个数组并获取他的metatable,metatable被赋值给metaarray(我们不能从Lua中设置userdata的metatable,但是我们在Lua中无限制的访问metatable)。接下来,我们设置metaarray.__index为metaarray。当我们计算a.size的时候,Lua在对象a中找不到size这个键值,因为对象是一个userdatum。所以,Lua试着从对象a 的metatable的__index域获取这个值,正好__index就是metaarray。但是metaarray.size就是array.size,因此a.size(a)如我们预期的返回array.size(a)。
当然,我们可以在C中完成同样的事情,甚至可以做得更好:现在数组是对象,他有自己的操作,我们在表数组中不需要这些操作。我们实现的库唯一需要对外提供的函数就是new,用来创建一个新的数组。所有其他的操作作为方法实现。C代码可以直接注册他们。
getsize、getarray和setarray与我们前面的实现一样,不需要改变。我们需要改变的只是如何注册他们。也就是说,我们必须改变打开库的函数。首先,我们需要分离函数列表,一个作为普通函数,一个作为方法:
static const struct luaL_reg arraylib_f [] = {
{"new", newarray},
{NULL, NULL}
};
static const struct luaL_reg arraylib_m [] = {
{"set", setarray},
{"get", getarray},
{"size", getsize},
{NULL, NULL}
};
新版本打开库的函数luaopen_array,必须创建一个metatable,并将其赋值给自己的__index域,在那儿注册所有的方法,创建并填充数组表:
int luaopen_array (lua_State *L) {
luaL_newmetatable(L, "LuaBook.array");
lua_pushstring(L, "__index");
lua_pushvalue(L, -2); /* pushes the metatable */
lua_settable(L, -3); /* metatable.__index = metatable */
luaL_openlib(L, NULL, arraylib_m, 0);
luaL_openlib(L, "array", arraylib_f, 0);
return 1;
}
这里我们使用了luaL_openlib的另一个特征,第一次调用,当我们传递一个NULL作为库名时,luaL_openlib并没有创建任何包含函数的表;相反,他认为封装函数的表在栈内,位于临时的upvalues的下面。在这个例子中,封装函数的表是metatable本身,也就是luaL_openlib放置方法的地方。第二次调用luaL_openlib正常工作:根据给定的数组名创建一个新表,并在表中注册指定的函数(例子中只有一个函数new)。
下面的代码,我们为我们的新类型添加一个__tostring方法,这样一来print(a)将打印数组加上数组的大小,大小两边带有圆括号(比如,array(1000)):
int array2string (lua_State *L) {
NumArray *a = checkarray(L);
lua_pushfstring(L, "array(%d)", a->size);
return 1;
}
函数lua_pushfstring格式化字符串,并将其放到栈顶。为了在数组对象的metatable中包含array2string,我们还必须在arraylib_m列表中添加array2string:
static const struct luaL_reg arraylib_m [] = {
{"__tostring", array2string},
{"set", setarray},
...
};