lua元表(metatable)和元方法(metamethod)
(一) 元表概念:
引言:Lua中的每个值都有一套预定义的操作集合,如数字相加等。但无法将两个table相加,此时可通过元表修改一个值的行为,使其在面对一个非预定义的操作时执行一个指定操作。
访问机制:一般的元方法都只针对Lua的核心,也就是一个虚拟机。它会检测一个操作中的值是否有元表,这些元表是否定义了关于这次操作的元方法。例如两个table相加,先检查两者之一是否有元表,之后检查是否有一个叫“__add”的字段,若找到,则调用对应的值。“__add”等即是字段,其对应的值(往往是一个函数或是table)就是“元方法”。
1, 元表实例
setmetatable(只能用于table)和getmetatable(用于任何对象)
语法:setmetatable (table, metatable),对指定table设置metatable 【如果元表(metatable)中存在__metatable键值,setmetatable会失败】
语法:tmeta = getmetatable (tab),返回对象的元表(metatable) 【如果元表(metatable)中存在__metatable键值,当返回__metatable的值】
1. 系统使用字段:
算术类元方法: 字段:__add(+), __mul(*), __ sub(-), __div(/), __unm, __mod(%), __pow, (__concat)
关系类元方法: 字段:__eq, __lt(<), __le(<=),其他Lua自动转换 a~=b --> not(a == b) a > b --> b < a a >= b --> b <= a 【注意NaN的情况】
table访问的元方法: 字段: __index, __newindex
__index:
查询:访问表中不存的字段
rawget(t, i)
__newindex:
更新:向表中不存在索引赋值
rawset(t, k, v)
2. 自定义字段:
上面字段是供系统使用的字段,比如当我们给元表的__add字段赋值的时候,那么当执行"t1 + t2"时,就会调用到__add操作;我们也可以定义我们自己需要的字段,比如使用lua构造的类最常定义的__new操作,可以通过定义此字段来完成类对象的创建, 等等.
例子:
运行结果:
"sub": - 操作。 行为类似于“add”操作。
"mul": * 操作。 行为类似于“add”操作。
"div": / 操作。 行为类似于“add”操作。
"mod": % 操作。 行为类似于“add”操作。以o1 - floor(o1/o2)*o2为操作原语。
"pow": ^ (取幂)操作。 行为类似于“add”操作,以函数pow(来自C数学库)为操作原语。
"unm": 一元-操作。
(二) 下面介绍rawget 和rawset
有时需要get 和set表的索引,不想使用metatable.你可能回猜想, rawget 允许你得到索引无需__index,rawset允许你设置索引的值无需__newindex (相对传统元表的方式,这些不会提高速度)。为了避免陷在无限循环里,你才需要使用它们。 在上面的例子里, t[key] = value * value将再次调用__newindex函数,这让你的代码陷入死循环。使用rawset(t, key, value * value) 可以避免。你可能看到,使用这些函数, 我们必须传递参数目标table, key, 当你使用rawset时还有value。
1, lua中使用元表和元方法进行类多继承的实现
local function search(k, plist) for i=1, #plist do -- 尝试第i个父类 local v = plist[i][k]; if v then return v end end end function createClass( ... ) -- 新类 local c = {} local parents = { ... } -- 在父类列表中搜索 setmetatable(c, { __index = function(t, k) return search(k, parents) end}) -- c表的__index指向其自身 c.__index = c -- 注释1 -- 新类的构造函数 ----[[ 注释2 function c:new(o) local o = o or {} -- 将c表作为其实例o的元表 setmetatable(o, c) return o end --]] -- 注释3 -- function c:getName() -- return "die" -- end return c end Named = {} -- 使用lua语法糖 ":", 隐式传递self function Named:getName() return self.name end function Named:setName(n) self.name = n end Account = {balance = 0} ----[[ 注释4 function Account:new(o) local o = o or {} setmetatable(o, self) return o end --]] -- 调用 NamedAccount = createClass(Named, Account) account = NamedAccount:new({name = "Paul", }) print(account:getName())
(注:开 --> 打开注释,代码失效;关 --> 关闭注释,代码生效。)
代码的工作流程:account:getname()语句, 首先,Lua在表account(为{ name = "Paul"}, 由createClass函数中调用new返回的o,也即此处的account表 = { name = "Paul"})中无法找到字段"getname"。因此,就查找account表的元表(根据语句"setmetatable(o, c)", 可知为account表的元表为NamedAccount表(c表),也即createClass函数最后返回的 c 表)的__index字段, 发现c表的__index字段指向c表自身,故而在c表中查找,这时同样没找到"getname"字段, 继续查找,此时开始进入c表的元表(account表的元表为 c 表(此例子中为NamedAccount表),c 表的元表为{__index = function(t, k) return search(k, parent) end})查找, 如下:
setmetatable(c, {__index = function(t, k) return search(k, parents) end})
由于这个字段是一个函数,Lua就调用了它。该函数先在c表的父表Account中查找"getname"。未找到后,继而查找Named父表。最终在Named中找到了一个非nil值,即为搜索的最终结果。
lua表的查找过程:
1. 先在当前表 curr_tbl 中查找,这里curr_t = {name = "Paul",}; 如果curr_t找到所需字段getName,返回结果,查找成功,停止向上查找;否则,进入步骤2;
2. 如果找不到所需字段, 如果当前表 curr_tbl 没有元表, 则找不到所需字段,查找失败,停止查找;否则,进入步骤3;
3. 如果 curr_tbl 有元表,则进入其元表(此处为c表)并找到__index字段,如果没有__index字段,则同样查找失败(所以,如果把上面注释1处的语句"c.__index = c"删除的话,则查找失败,且不会再进一步向上查找);否则, 进入步骤4;
4. 如果有__index字段,则从__index字段所指定的表 1_tbl 重新开始执行查找(上面例子中, c表的__index字段指定的表是其自身, 所以是在c表中查找), 此时如果c表有所需字段,如注释3所示,那么便在c表中查找到了所需字段,查找成功,同时停止向上查找; 否则, 进入步骤5;
5. 在c表并没有找到所需字段, 如果c表没有指定元表,则查找失败,停止向上查找; 否则, 进入步骤6;
6. c表指定了元表 2_tbl, 进入 2_tbl并找到__index字段, 如果在 2_tbl中找不到__index字段, 插找失败,返回并停止查找; 否则, 进入步骤7;
7. 进入__index字段指定的表中查找, 和步骤4一样; 但此时 2_tbl 的__index字段指定的是一个搜索函数, 那么就进入此搜索函数进行搜索;
8. 在search函数中, 当查找到了"NamedAccount = createClass(Named, Account)"中的父类Named的时候,找到了所需字段getName, 查找成功,返回结果并停止查找.
注: 去掉“2”号注释处的代码,程序会调用到“4”号注释处的代码;