美文网首页
Lua 元表简述

Lua 元表简述

作者: 甚解_4703 | 来源:发表于2018-09-17 14:15 被阅读0次

    补充一下metatable (元表)的知识。
    lua中的table拥有一些列可预见的操作。
    比如申明一个普通的table:a = {x=1,y=2},如果我们访问a.x,那么打印1,如果我们访问a.y,那么打印2,如果我们访问a.z,那么打印nil。但是如果我们想要a.z有值呢?当然不是简单的a.z = xx赋值这么简单的方法。这时候就需要我们的元表了。
    lua提供了内建函数getmetatablesetmetatable
    lua的元表可以是自身,也可以是别的table:

    > a = {x=1,y=2}
    > setmetatable(a,a) -- 设置a的元表为自己
    table: 003ea930 -- 返回table a
    > getmetatable(a) -- 返回a的元表
    table: 003ea930
    > b = {z=3}
    > setmetatable(a,b) -- 设置a的元表为b
    table: 003ea930
    > getmetatable(a)
    table: 003eb328
    > a.z -- 猜猜看我们能打印出3吗?
    

    好,既然上面不能打印3那就疑问了,我们设置这个干啥呢?这个需要配合元方法使用!上面这个例子后面还会说,我们先来看最简单的元方法-算术元方法。
    举个例子,通常我们想要两个table实现+运算是不行的:

    > a = {1}
    > b = {2}
    > a + b
    stdin:1: attempt to perform arithmetic on a table value (global 'a')
    stack traceback:
            stdin:1: in main chunk
            [C]: in ?
    

    好,下面我们来定义一个集合,保存为set.lua文件:

    Set = {}
    
    Set.mt = {}
    
    function Set.new(t)
        local set = {}
        setmetatable(set,Set.mt)
        for _,v in pairs(t) do 
            table.insert(set,v) 
        end
        return set
    end
    
    function Set.add(a,b)
        local ret = Set.new{} -- 等价于 Set.new({}),以前提过
        for _,v in pairs(a) do 
            table.insert(ret,v)  
        end
        for _,v in pairs(b) do 
            table.insert(ret,v)  
        end
        return ret
    end
    
    Set.mt.__add = Set.add -- 注意这里的 __add
    
    

    我们引入这个模块,然后执行我们的加法:

    > require "set"
    true
    > s1 = Set.new{1,2}
    > s2 = Set.new{4,5}
    > s3 = s1 + s2
    > for k,v in pairs(s3) do print(k,v) end
    1       1
    2       2
    4       4
    5       5
    

    到这里,我们发现我们可以加法了呢。。。为啥呢???
    仔细看我们上面的元表Set.mt,我们对它增加了一个方法Set.mt.__add,如果我们尝试把它赋值为nil:

    > Set.mt.__add = nil
    > s1 + s2 -- 报错了。
    stdin:1: attempt to perform arithmetic on a table value (global 's1')
    stack traceback:
            stdin:1: in main chunk
            [C]: in ?
    > Set.mt.__add = Set.add -- 这里再改回来!
    

    所以呢,我们两个Set能相加,多亏了我们的__add方法。我们称之为元方法,类似的方法还有很多:

    更多重载

    另外,lua对于自定义类型(table)的操作运算遵循:

    1. 如果第一个值有元表,并且有对应的操作符重载,lua选择它作为这次运算的方法。不依赖第二个值
    2. 否则,如果第二个值有元表,并且有对应的操作符重载,lua选择它作为这次运算的方法。
    3. 否则,lua raises an error(报错)

    按照上面的规则,1+s1s1+1是等价的,想一想为什么?

    如果我们直接去运算 s1+1会报错,我们需要加条件判断:

    function Set.add(a,b)
        -- 增加对a和b的判断
        if getmetatable(a) ~= Set.mt or getmetatable(b) ~= Set.mt then
            error("attempt to `add' a set with a non-set value")
        end
    
        local ret = Set.new{} -- 等价于 Set.new({})
        for _,v in pairs(a) do 
            table.insert(ret,v)  
        end
        for _,v in pairs(b) do 
            table.insert(ret,v)  
        end
        return ret
    end
    

    再试一下:

    > s1 + 1 -- 现在就是报我们自己的error了
    .\set.lua:17: attempt to `add' a set with a non-set value
    stack traceback:
            [C]: in function 'error'
            .\set.lua:17: in metamethod '__add'
            stdin:1: in main chunk
            [C]: in ?
    

    还有关系元方法:

    __eq 重载 ==  -- 没有~=,可以用 not ==
    __lt 重载 <   -- 没有>,可以换个方向 
    __le 重载 <=  -- 没有>=,可以换个方向 
    

    改造一下我们的脚本:

    function Set.check(a,b)
        -- 增加对a和b的判断
        if getmetatable(a) ~= Set.mt or getmetatable(b) ~= Set.mt then
            error("attempt to operate a set with a non-set value")
        end
    end
    
    function Set.le(a,b)
        Set.check(a,b)
        return #a <= #b
    end
    
    
    function Set.lt(a,b)
        return a <= b and not (b <= a)
    end
    
    function Set.eq(a,b)
        return a <= b and b <= a
    end
    
    Set.mt.__le = Set.le
    Set.mt.__lt = Set.lt
    Set.mt.__eq = Set.eq
    

    退出重新引入一下:

    > Set.new{1,2,3} == Set.new{4,5,6} -- 长度一致
    true
    > {1,2,3} == {4,5,6} -- 没有重载就会去判断对象是否同一个
    false
    > {1,2,3} == {1,2,3} -- 这里跟python不同
    false
    

    关系元方法跟算术元方法不一样,它不支持混合类型。如果你比较两个不同类型的数据(string和number)的大小,或者拥有不同关系元方法的对象的大小,lua将报错,但是==是另外。==的两边如果是不同类型的数据,那么直接返回false,或者都是table,但是他们的关系元方法不一样,那么也是返回false,仅当都是对象(table),且关系元方法一致,那么lua将会调用这个元方法。

    还有一些其他的元方法:

    __tostring  重载内建函数 tostring()
    __metatable 重载内建函数 getmetatable()
    

    看例子:
    对我们的脚本增加下面代码:

    function Set.checkset(a)
        if getmetatable(a) ~= Set.mt then
            error("attempt to operate with a non-set value")
        end
    end
    
    function Set.tostring(a)
        Set.checkset(a)
        local ret = "{"
        local sep = ""
        for k,v in pairs(a) do 
            ret=ret..sep..v
            sep = ","
        end
        return ret.."}"
    end
    

    重新引入:

    > s1 = Set.new{1,2,3}
    > s1
    {1,2,3} -- 这就是我们想要的打印.
    
    -- 如果不想别人修改你的metatable,那么我们可以定义一个__metatable
    > Set.mt.__metatable = "not your business"
    > getmetatable(s1)
    not your business
    > setmetatable(s1,s1) 
    stdin:1: cannot change a protected metatable
    stack traceback:
            [C]: in function 'setmetatable'
            stdin:1: in main chunk
            [C]: in ?
     
    -- 无法修改保护的metatable,OK目的达成
    

    以上基础元方法已经介绍完毕,最后看两个特殊的元方法__index__newindex,它们就可以完成我们一开始的a.z任务_
    所有的table,它们在访问成员的时候其实通过__index
    方法去找的,如果找到返回这个值,如果没找到返回nil。
    先来看看我们的最新脚本文件set.lua:

    Set = {}
    
    Set.mt = {}
    Set.mt.__metatable = "sorry abourt this"
    
    function Set.new(t)
        local set = {}
        setmetatable(set,Set.mt)
        for _,v in pairs(t) do 
            table.insert(set,v) 
        end
        return set
    end
    
    function Set.checkset(a)
        if getmetatable(a) ~= Set.mt.__metatable then
            error("attempt to operate with a non-set value")
        end
    end
    
    function Set.check(a,b)
        -- 增加对a和b的判断
        if getmetatable(a) ~= Set.mt.__metatable 
            or getmetatable(b) ~= Set.mt.__metatable then
            error("attempt to operate a set with a non-set value")
        end
    end
    
    function Set.add(a,b)
        Set.check(a,b)
        local ret = Set.new{} -- 等价于 Set.new({})
        for _,v in pairs(a) do 
            table.insert(ret,v)  
        end
        for _,v in pairs(b) do 
            table.insert(ret,v)  
        end
        return ret
    end
    
    function Set.le(a,b)
        Set.check(a,b)
        return #a <= #b
    end
    
    
    function Set.lt(a,b)
        return a <= b and not (b <= a)
    end
    
    function Set.eq(a,b)
        return a <= b and b <= a
    end
    
    function Set.tostring(a)
        Set.checkset(a)
        local ret = "{"
        local sep = ""
        for k,v in pairs(a) do 
            ret=ret..sep..v
            sep = ","
        end
        return ret.."}"
    end
    
    
    Set.mt.__add = Set.add
    Set.mt.__le = Set.le
    Set.mt.__lt = Set.lt
    Set.mt.__eq = Set.eq
    Set.mt.__tostring = Set.tostring
    
    

    重新引入文件,然后看这个例子:

    > s1 = Set.new{1,2,3} -- 申明一个新的Set
    > s1 -- 看打印
    {1,2,3}
    -- 我们申明__index函数,打印参数table和key,并返回nil
    > Set.mt.__index = function(tab,key) print(tab,key) return nil end
    > s1.a -- 跟原来一样
    a
    > s1.b -- 区别来了。。。
    {1,2,3,a}       b
    nil
    

    通过上面的例子我们看到,当调用s1里面含有的key的时候跟原来一样,但是s1.b的时候,由于我们的s1里面并不含有,所以就调用了我们给__index赋值的函数,打印输出,当然结果还是nil.
    回到最开始的例子(a.z):

    > a = {x=1,y=2}
    > b = {z=3}
    > setmetatable(a,b)
    table: 003eee58
    > b.__index = function(tab,key) return b[key] end
    > a.z
    3 -- OK 我们想要的结果来了~
    

    其实我们的__index可以是function也可以是table,如果是function,那么会把调用的table和key作为参数传过去;如果是table,那么lua只是重新查询,如果查到返回,没有则继续调用这个table的__index元方法。
    利用这一点我们可以完成类的继承这样的功能。具体后面再细说。
    如果我们只是想访问这个table的元素,避开它的__index调用,我们可以调用内建函数rawget(table,key):

    > rawget(a,"x") -- 避开__index
    1
    > rawget(a,"z") -- 避开__index
    nil
    

    rawget函数并不能给我们的程序提速,但是我们有时候需要用它。

    __newindex元方法是更新table,当我们给table赋值一个本不存在的key的时候,lua解释器先去找这个table的__newindex,如果不为nil,则调用它;如果为nil,就对table做赋值操作:

    > a = {}
    > setmetatable(a,a)
    table: 0033aa88
    > a.__newindex = function(tab,k,v) return print(tab,k,v) end
    > a.x = 1
    table: 0033aa88 x       1
    >a.x
    nil
    

    __newindex的值可以是table,这样赋值操作将会在那个table上发生,原table不会做任何事。

    > a = {}
    > b = {}
    > setmetatable(a,b)
    table: 00c6f0a0
    > b.__newindex = b -- 想想这里能不能赋值 a ?
    > a.x = 1
    > a.x
    nil
    > b.x
    1
    > rawset(a,'y',2) -- 这个跟rawget有点类似,避开__newindex
    table: 0068a9c0
    > a.y
    2
    > b.y
    nil
    > b.__index = b
    > a.x
    1
    

    table的默认值是nil,我们可以用__index修改这个默认值,想想可以怎么做?

    利用__index__newindex我们可以监视某个空table的操作:

    -- create private index
    local index = {}
    
    -- create metatable
    local mt = {
      __index = function (t,k)
        print("*access to element " .. tostring(k))
        return t[index][k]   -- access the original table
      end,
    
      __newindex = function (t,k,v)
        print("*update of element " .. tostring(k) ..
                             " to " .. tostring(v))
        t[index][k] = v   -- update original table
      end
    }
    
    function track (t)--监视函数,要监视某个table,只需t=track(t)
      local proxy = {}
      proxy[index] = t
      setmetatable(proxy, mt)
      return proxy
    end
    

    把table变为只读:

    function readOnly (t)
      local proxy = {}
      local mt = {       -- create metatable
        __index = t,
        __newindex = function (t,k,v)
          error("attempt to update a read-only table", 2)
        end
      }
      setmetatable(proxy, mt)
      return proxy
    end
    

    使用方法:

    days = readOnly{"Sunday", "Monday", "Tuesday", "Wednesday","Thursday", "Friday", "Saturday"}
        
    print(days[1])     --> Sunday
    days[2] = "Noday"
    stdin:1: attempt to update a read-only table
    

    最后讲一下这个 __call 元方法

    > a = {} 
    > mt = {}
    > mt.__call = function(...) print("call mt.__call",...) end --定义我们的元方法
    > a(1,2,3) --因为我们的a是一个table,所以直接这样调用是不能的。
    stdin:1: attempt to call a table value (global 'a')
    stack traceback:
            stdin:1: in main chunk
            [C]: in ?
    
    
    > setmetatable(a,mt) --这里设置一下元表
    table: 00bdcce0
    > a(4,5,6)
    call mt.__call  table: 00bdcce0 4       5       6
    > mt
    table: 00bdcc18
    > a
    table: 00bdcce0
    >
    

    __call在调用的时候,默认会把对象作为第一个参数也传入进去,上面的例子就是把a也传入进去。所以我们定义__call的时候可以写成下面这样:

    mt.__call = function(ins,...) print("ins=",ins,...) end
    

    相关文章

      网友评论

          本文标题:Lua 元表简述

          本文链接:https://www.haomeiwen.com/subject/roelnftx.html