正如我们之前所看到的,当访问一个表中不存在的字段时会得到 nil
。这是正确的,但不是完整的真相。实际上,这些访问会引发解释器查找一个名为 __index
的元方法。如果没有这个元方法,那么像一般情况下一样,结果就是 nil
;否则,则由这个元方法来提供最终结果。
下面介绍一个关于继承的原型示例。假设我们要创建几个表来描述窗口,每个表中必须描述窗口的一些参数,例如位置、大小及主题颜色等。所有的这些参数都有默认值,因此我们希望在创建窗口对象时只需要给出那些不同于默认值的参数即可。第一种方法是使用一个构造器来填充不存在的字段,第二种方法是让新窗口从一个原型窗口继承所有不存在的字段。首先,我们声明一个原型:
-- 创建具有默认值的原型 prototype = {x = 0, y = 0, width = 100, height = 100}
然后,声明一个构造函数,让构造函数创建共享同一个元表的新窗口:
local mt = {} -- 创建一个元表 -- 声明构造函数 function new (o) setmetatable(o, mt) return o end
现在,我们来定义元方法 __index
:
mt.__index = function (_, key) return prototype[key] end
在这段代码后,创建一个新窗口,并查询一个创建时没有指定的字段:
w = new{x = 10, y = 20} print(w.width) --> 100
Lua
语言会发现 w
中没有对应的字段 "width"
,但却有一个带有 __index
元方法的元表。因此, Lua
语言会以 w
(表)和 "width"
(不存在的键)为参数来调用这个元方法。元方法随后会用这个键来检索原型并返回结果。
在 Lua
语言中,使用元方法 __index
来实现继承是很普遍的方法。虽然被叫做方法,但元方法 __index
不一定必须是一个函数,它还可以是一个表。当元方法是一个函数时, Lua
语言会以表和不存在的键为参数调用该函数,正如我们刚刚看到的。当元方法是一个表时, Lua
语言就访问这个表。因此,在我们此前的示例中,可以把 __index
简单地声明为如下样式:
mt.__index = prototype
这样,当 Lua
语言查找元表的 __index
字段时,会发现字段的值是表 prototype
。因此, Lua
语言就会在这个表中继续查找,即等价地执行 prototype["width"]
,并得到预期的结果。
将一个表用作 __index
元方法为实现带继承提供了一种简单快捷的方法。虽然将函数用作元方法开销更昂贵,但函数却更加灵活:我们可以通过函数来实现多继承、缓存及其他一些变体。
如果我们希望在访问一个表时不调用 __index
元方法,那么可以使用函数 rawget
。调用 rawget(t, i)
会对表 t
进行原始的访问,即在不考虑元表的情况下对表进行简单的访问。进行一次原始访问并不会加快代码执行(一次函数调用的开销就会抹杀用户所做的这些努力),但是,我们后续会看到,有时确实会用到原始访问。