美文网首页
lua入门笔记3 元表(metatable)与元方法(metam

lua入门笔记3 元表(metatable)与元方法(metam

作者: Charon_ted | 来源:发表于2019-09-25 16:12 被阅读0次
    • 元表的概念

      通常,lua中的每个值都会有一套预定义的操作集合。例如可以将两个数字相加,但是我们没有办法直接让两个table相加,也没有办法对函数作比较,或者调用一个字符。但是我们可以通过元表来修改一个值的行为,使其在面对一个非预定义的操作时执行一个指定的操作。

      例如,我们有两个table a,b 可以通过元表定义出如何计算a+b。当lua试图将两个table相加时,他会先检查二者之一是否有元表,然后检查该元表中是否有一个叫_add的字段。如果找到了该字段,就调用该字段对应的值,这个值也就是所谓的元方法。也就是c++中的运算符重载

    • 注意事项

    1. lua里每一个值都有一个元表,不同的是tableuserdata可以有各自独立的元表(两个table可以分别对应两个元表),而其他类型的值则共享其类型所属的单一元表。
    t={}
    print(getmetatable(t))   -->nul  lua在创建新的table时,不会创建元表
    
    t1={}
    t.setmetatable(t,t1)      -->将t1设置为t的元表  
    
    1. 任何的table都可以作为任何值的元表,一组table可以共用一个元表。这个元表描述了他们共同的行为。一个table甚至可以作为他自己的元表,用于描述他自己特有的行为。

    2. lua里,你只能设置table的元表。如果想设置其他的类型必须通过C代码来实现。

    • 算数类的元方法

    我们这里先看一个示例

    Set={}
    
    mt={}
    
    function Set.New(t)
        local res={}
        setmetatable(res,mt)
        for k,v in ipairs(t) do
            res[k]=v;
        end
        return res
    end
    
    function Set.Add(a,b)
        local res=Set.New({})
        for k,v in ipairs(a)    do res[k]=v end
        for k,v in ipairs(b)    do res[k]=v end
        return res
    end
    
    
    function Set.PrintToString(a)
        res="{ "
        for k,v in ipairs(a)   do   res=res..v.." " end
        res=res.." }"
    
        return res
    end
    
    mt.__add=Set.Add
    mt.__concat=Set.PrintToString
    
    
    a=Set.New({1,2,3,4,5})
    b=Set.New{5,6,7}
    
    c=a+b
    print(Set.PrintToString(c))
    
    

    输出结果为 { 5 6 7 4 5 }
    这里是一个简单地案例,修改了两个table相加的元方法。
    这里需要注意的是,当两个集合相加时,可以使用任意一个集合的元表。然而,当一个表达式中混合了不同元表的值时,他会按照以下步骤查找元表:如果第一个有元表,并且元表中有__add字段,lua就以此字段作为元方法;如果两个值都没有元方法,Lua就引发一个错误
    同理,我们也可以在此基础上对其他的操作进行重载,例如减法,乘法等...

    • 关系类的元方法

    关系类的元方法有三个,分别为__eq(等于)_lt(小于)__le(小于等于),其他三个没有单独的方法,通过一下转换得出。

    a~=b  ==> not(a==b)
    a>b  ==>  b<a
    a>=b  ==>  b<=a 
    

    与算术类元方法不同的是,关系类元方法不能应用于混合类型。如果试图这样操作,lua会引发一个错误
    等于比较永远不会引发错误。但是如果两个对象拥有了不同的元方法,那么等于操作不会调用任何一个元方法,而是直接返回false。只有当两个比较对象共享一个元方法是,lua才调用这个等于比较的元方法。

    • 库定义的元方法

      目前介绍的元方法只针对lua的核心,也就是一个虚拟机。由于元表也是一种常规的table,所以任何人、任何函数都可以使用他们。
      函数print总是调用tostring来格式化其输出。(tostring ==> __tostring)
      函数setmetatablegetmetable也会用到用到元表中的一个字段,用于保护元表。假设想要保护器元表,使用户及看不见也不能修改集合的元表,那么就需要用到字段__metatable。当设置了该字段时,getmetatable就会返回该段的值,而setmetatble会引发一个错误。

    • table访问的元方法

    lua还提供了一种可以改变table行为的方法。有两种可以改变的table行为:查询table以及修改table中不存在的字段。

    1. __index元方法

    当访问一个table中不存在的字段时,得到的结果是nil。但实际上,这些访问会促使解释器去查找一个叫__index的元方法。如果没有这个元方法,那么访问结果如前所述的为nil。否则,就由这个元方法来提供最终结果。

    来看一个比较经典的示例

    Window={}
    Window.prototype={x=0,y=0,width=100,height=100}
    Window.mt={}
    
    Window.mt.__index=function(table,key)
        return Window.prototype[key]
    end
    
    function Window.new(o)        --类似于构造函数
        setmetatable(o, Window.mt)
        return o
    end
    
    print((Window.new{x=10,y=20}).width)
    

    这里要创建一些窗口的table,每个table中必须描述一些窗口参数,所有的这些参数都有默认值。当没有给对应参数赋值是,返回默认值。这里相当于是一个简单地继承。所有生成的窗口继承了prortotype

    这个示例中__index元方法是一个函数。但其实他还可以是一个table。当他是一个table时,lua就会在这个table中查询这个key所对应的的value

    虽然将函数作为__index 来实现相同功能开销较大,但函数更加灵活。可以通过函数来实现多重继承、缓存及其他一些功能。
    如果不想在访问一个table时涉及到他的__index元方法,可以使用函数rawget调用rawget(t,i)就相当于对table进行了一个原始的访问,不考虑元表

    2.__newindex元方法

    __newindex元方法与__index类似,不同之处在于前者用于table的更新(set)后者用于table的查询(get)。当对一个table中不存在的索引赋值时,解释器就会查找__newindx元方法。如果有,解释器就会调用它而不是执行赋值。

    3.具有默认值的table

    常规table中的任何字段默认都是nil。通过元表我们可以很容易地修改这个默认值:

    function setDefault(t,d)
      local mt=(__index=function() return d end)
      setmetatable(t,mt)
    end
    
    tab ={x=10,y=20}
    print(tab.x,tab.z)    -->10  nil
    setdefalut(tab,0)
    print(tab.x,tab.z)    -->10 0
    

    在调用setdefault后,任何对tab中存在字段的访问都将调用他的__index元方法,而这个元方法会返回0(这个原方法中d的值)
    这里setDefault函数为了所有需要默认值的table创建了一个新的元表,如果需要很多带默认值的table其开销会比较大。这里我们可以换一种写法
    这里我们把 默认值存放在table本身中,_ _ _用这种相对特殊的命名防止冲突

    local mt={__index=function(t) return t.___ end}
    function setDefault(t,d)
      t.___=d
      setmetatable(t,mt)
    end
    

    防止冲突还有一种写法,确保这个特殊key的唯一性

    local key={}      --唯一key
    
    local mt={__index=function(t) return t.[key] end}
    function setDefault(t,d)
      t.[key]=d
      setmetatable(t,mt)
    end
    
    
    4. 跟踪table的访问

    _index_newindex 都是在table中没有需要访问的index才发挥作用的。因此只有讲一个table保持空,才可能捕捉到所有对他的访问。

    t={}    --原来的table
    local _t=t;     --保持一个对原来的table的引用
    t={}            --创建代理
    
    local mt={
        __index=function(t,k)
            print("get element"..k)
            return _t[k]      --从本来的table中获取get数值
        end,
        __newindex=function(t,k,v)
            print("set element"..k)
            _t[k]=v            --像本来的table中set数值
        end
    }
    setmetatable(t, mt)
    
    t[1]=1
    t[2]=2
    print(t[1])
    

    输出结果:

    set element1
    set element2
    get element1
    1

    但这种写法有一个弊端:无法遍历原本的table。pairs只能够访问到代理的table

    但是这里又有一个问题。如果我们需要同时监视多个table,其实无需创建多个不同的元表。我们只要以某种形式让每个代理和元表关联起来,并且所有代理都共享一个公共的元表,这里上代码

    local index={}                  --当做一个不容易重复的key值
    
    local mt={
        __index=function(t,k)
            print("get data"..k)
            return t[index][k]
        end,
        __newindex=function(t,k,v)
            print("set data"..k)
            t[index][k]=v
        end
    }
    
    function track(t)
        local proxy={}                  --此处每次都会创建新的table,此table会作为代理返回
        print("proxy adress is "..tostring(proxy))
        print("index adress is "..tostring(index))
        proxy[index]=t                  --真正的数据,放在table内某个key值能难重复的地方
        setmetatable(proxy, mt)         
        return proxy
    end
    
    t={}
    t=track(t)
    t[1]=45
    print(t[1])
    t1={}
    t1=track(t1)
    print(t1[1])
    

    所有的代理都存放在元表内,而真正的表又在代理内。外部无法直接获取到对应的值

    5. 只读的table

    我们可以通过代理概念,很轻松的创造出实现只读的元表。具体只需要在__newindex是取消更新操作并引发一个错误提示。具体代码不在演示。


    最后所有重载的一览

    函数名 意义&注意事项
    _add + 操作,如果任何不是数字的值(包括不能转换为数字的字符串)做加法, Lua 就会尝试调用元方法。 首先、Lua 检查第一个操作数(即使它是合法的), 如果这个操作数没有为 "__add" 事件定义元方法, Lua 就会接着检查第二个操作数。 一旦 Lua 找到了元方法, 它将把两个操作数作为参数传入元方法, 元方法的结果(调整为单个值)作为这个操作的结果。 如果找不到元方法,将抛出一个错误。
    __sub - 操作。 行为和 "add" 操作类似。
    __mul * 操作。 行为和 "add" 操作类似。
    __div / 操作。 行为和 "add" 操作类似
    __mod % 操作。 行为和 "add" 操作类似
    __pow ^ (次方)操作。 行为和 "add" 操作类似
    __unm - (取负)操作。 行为和 "add" 操作类似。
    __concat .. (连接)操作。 行为和 "add" 操作类似, 不同的是 Lua 在任何操作数即不是一个字符串 也不是数字(数字总能转换为对应的字符串)的情况下尝试元方法。
    __idiv // (向下取整除法)操作。 行为和 "add" 操作类似。
    __band & (按位与)操作。 行为和 "add" 操作类似, 不同的是 Lua 会在任何一个操作数无法转换为整数时 (参见 §3.4.3)尝试取元方法。
    __bor |(按位或)操作。 行为和 "band" 操作类似。
    __bxor ~ (按位异或)操作。 行为和 "band" 操作类似。
    __bnot ~ (按位非)操作。 行为和 "band" 操作类似。

    lua手册

    相关文章

      网友评论

          本文标题:lua入门笔记3 元表(metatable)与元方法(metam

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