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函数最后返回的 表)的__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”号注释处的代码;

 

posted @ 2014-07-26 13:08  小天_y  阅读(3711)  评论(0编辑  收藏  举报