zoukankan      html  css  js  c++  java
  • 《Programming in Lua 3》读书笔记(十)

    这一部分应该挺重要的,Lua中唯一的数据结构便是table,几乎所有的的数据操作都是在table的基础上进行。而本文提到的元表和元方法,便是帮助table实现更强大的功能而设计的。

    日期:2014.7.11

    Part Ⅱ 

    Metatables and Metamethods

    Lua中不能直接对table进行相加、比较等操作。除非使用元表(Metatables)。元表可以使得我们改变元素在处理未定义操作的应对行为,如定义两个table直接的相加操作。Lua在处理两个table的相加操作时会首先检查两个table是否有元表,且元表中是否有 __add 元方法字段,如果有这个字段则lua会遵循这个字段内定义的操作执行两个table的相加操作。

    Lua中各个类似的变量有一个相关联的元表?(到底有没有?),而table与userdata有各自独立的元表。默认的,新创建的table是没有元表的:
    e.g.
    t = {}
    print(getmetatable(t))          --nil
    此时我们可以通过setmetatable方法来设置元表,元表其实也相当于一个table
    e.g.
    t1 = {}
    setmetatable(t,t1)
    print(getmetatable(t) == t1)          --ture

    当然,在lua中我们只可以对table执行setmetatable操作,对其余类型的变量执行这个操作需要使用C 代码。书上string库涉及到了给string类型变量执行设置元表的操作。其余类型的变量默认是没有元表的?
    print(getmetatable("ss"))               -- table
    print(getmetatable(10))                  --nil


    Arithmetic Metamethods
    算术运算元方法

    这里介绍元表的使用,在这里用一个table表示set,我们需要运算set的并集等操作
    e.g.
    Set = {} 
    local mt = {}          --metatable for sets
    
    function Set.new(l)     --新建一个set,初始化并且设置其元表
         local set = {}     
         setmetatable(set,mt)
         for _ v in ipairs(l) do set[v] = true end
         return set
    end

    这样每次我们新建一个set其都会有同样的元表:
    s1 = Set.new{10,20,11,13}
    s2 = Set.new{30,1}
    print(getmetatable(s1))          --table: 0x7fa1eb4093a0
    print(getmetatable(s2))          --table: 0x7fa1eb4093a0


    给元表添加元方法:__add 字段表示table如果执行相加操作
    mt.__add = Set.union     --注意此时的 __add 字段还是不能使用的,因为Set.union 还未定义,而且这段代码也需要放在定义Set.union之后,否则会报错。正确的用法是先定义再赋值
    --假若mt.__add = Set.union
    --定义Set.union
    function Set.union( a,b )
         local res = Set.new{}
         for k in pairs(a) do
              res[k] = true
         end
         for k in pairs(b) do
              res[k] = true
         end
    
         return res
    end
    --此时
    s3 = s1 + s2 会报错     --attempt to perform arithmetic on global 's1' (a table value)
    --需要将mt.__add = Set.union 放置在定义Set.union之后。

    同样的,设置 __mul 元方法也是类似的要求
    --定义 Set.intersection
    function Set.intersection( a,b )
         local res = Set.new{}
         for k in pairs(a) do
              res[k] = b[k]
         end
         return res
    end
    mt.__mul = Set.intersection

    所有的算术运算元方法:
    __add(加)、__mul(乘)、__sub(减)、__div(除)、__unm(负)、__mod(取模)、__pow(取幂)、__concat(连接)

    Lua在处理两个变量的算术运算时,针对不同类型的变量,如
    e.g.
    s = Set.new {1,2,3}
    s = s + 8
    此时运行的话会报错:
    --bad argument #1 to 'pairs' (table expected, got number) 
    其处理两个变量的算术运算遵循的步骤是:如果第一个变量定义了元方法则使用第一个变量的元方法,而不会再考虑第二个元素的元方法;第一个没有而第二个有,则使用第二个的;否则就会报错。
    因此为了更好的控制程序运行,我们需要限制两个变量为同类型拥有同一个元表,以__add为例,可以如下操作:
    function Set.union( a,b )
         if getmetatable(a) ~= mt or getmetatable(b) ~= mt then
              error("xxx",2)     --注意这里的参数是2,表示是提醒用户是传递的参数有问题
         local res = Set.new{}
         for k in pairs(a) do
              res[k] = true
         end
         for k in pairs(b) do
              res[k] = true
         end
    
         return res
    end


    Relational Metamethods
    关系运算元方法
    Lua中的关系运算元方法主要有:
    __eq(相等)、__lt(小于)、__le(小于或等于)。而对于其他的关系操作符,Lua直接做了转换:a ~= b 相当于 not (a == b) 、a > b 相当于 b < a、a >= b 相当于 b <= a;
    关系运算元方法的具体使用类似于上文提到的算术运算元方法;
    要注意的是,当两个变量的元方法不同的时候执行相等关系运算会返回false;


    Library-Defined Metamethods
    库定义的元方法

    __tostring 元方法 
    当调用tostring函数的时候,函数首先会寻找变量是否有__tostring 元方法:
    同上文:
    s = Set.new{1,2,2}
    print(s)                                --table: 0x7f8349403d30
    print(getmetatable(s))           --table: 0x7f8349409fd0
    此时打印出来的并不是其值,但也不是其元表

    --to string
    function Set.tostring( set )
         local l = {}
         for e in pairs(set) do
              l[#l + 1] = e
         end
         return "{" .. table.concat(l," , ") .. "}"
    end

    mt.__tostring = Set.tostring
    print(s)                                   --{1,2,2}
    在设置了其元方法之后,才会正确的打印出其值

    当然我们可以通过一定的方法来保护我们的元表,setmetatabe和getmetatable也是使用到了元方法,我们可以根据这一特性达到我们的目的:
    e.g.
    mt.__metatable = "cannot change"
    s1 = Set.new{}
    print(getmetatable(s1))          --cannot change
    而当我们想改变其元表的时候
    e.g.
    setmetatable(s1,mt)               --error:cannot change a protected metatable 
    会报错,不能修改其元表,这样就达到了我们要保护元表的目的


    Table-Access Metamethods
    Lua允许通过元表来控制修改和访问table中不存在的元素的行为

    The __index metamehod
    当我们试图访问一个table中不存在的元素时,我们得到的值将会是nil。这是一般意义上将的,事实上,我们访问table中不存在的元素的时候,会触发编译器寻找__index 元方法,当没有该方法的时候会返回nil;而当该元方法存在被定义了,将会返回该方法定义的操作。
    这一个特性对我们使用继承机制,继承默认变量的时候有很大的帮助,书上也是以这个为例子做了讲解:
    --有默认变量的table
    prototype = {x = 0,y = 0,width = 100,height = 100}
    --元表
    mt = {}
    --构造函数
    function new(o)
         setmetatable(o,mt)
         return o
    end
    --定义元方法
    mt.__index = function(_,key)
         return prototype[key]
    end
    --创建一个新的table,使用继承机制,需要技能默认变量的table
    w = new{x = 10,y = 20}
    print(w.width)           --100
    此时w使用到了prototype里面的值

    __index 元方法不一定需要是一个函数,也可以是一个table。当该方法是一个函数的是,Lua会执行函数里定义的操作,当是一个table的时候,Lua直接在table中执行访问操作。

    函数 rawget(t,i) 可以使得我们访问table各个元素的时候不去调用 __index 操作.将会对t执行raw访问?啥意思

    The __newindex metamethod
    该元方法的作用表现在更新table中元素值的时候。当我们试图给table中不存在的键赋值的时候,编译器会寻找 __newindex 的元方法,如果有编译器将会执行该方法定义的操作,否则就会直接赋值。这里也有一个函数 rawset(t,k,v),该函数会绕开元方法,直接在t中对键k设置值v。
    书中提到,有效的结合__index 和 __newindex 两个元方法的使用将会带来很强大的设计技巧,如创建只读table,带默认值的table等。

    Tables with default values
    table带有默认的值,其原理主要是在我们访问一个table中不存在或者没有赋值的键的时候,返回值是一个固定值,这里就涉及到了 __index 元方法
    e.g.
    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
    setDefault(tab,0)
    print(tab.x,tab.z)               --10,0
    以上操作就为tab设置了默认值0,如果试图访问tab中没有定义或者不存在的元素,返回值将会是0

    为多个不同的table执行设置多个不同默认值的操作
    e.g.
    local mt = {__index = function (t) return t.___ end}
    function setDefault (t,d)
         t.___ = d
         setmetatable(t,mt)
    end
    这里的技巧在于,元表的定义在函数的外部,且将默认值存储在了要设置默认值的table本身的内部。
    避免命名冲突的操作:
    e.g.
    local key = {}          --以一个table作为key
    local mt = {__index = function (t) return t[key] end}
    function setDefault (t,d)
         t[key] = d
         setmetatable(t,mt)
    end


    Tracking table accesses
    有效的使用__index 和 __newindex 可以帮助我们监控对table的访问和赋值操作。结合使用 proxy (代理) 便可以追踪所有对table的访问操作并且追踪到其访问的值。书上提到的只有当table为空的时候才能捕获到所有对其的访问操作,为啥?
    t = {}     --original table
    --keep a private access to the original table
    local _t = t
    --create proxy 代理
    t = {}
    --create metatable
    local mt = {
         __index = function ( t,k )
              print("*access to element " .. tostring(k))
              return _t[k]
         end,
    
         __newindex = function ( t,k,v )
              print("*update of element " .. tostring(k) .. " to " .. tostring(v))
              _t[k] = v      --update original table
         end
    }
    setmetatable(t,mt)
    
    t[2] = "hello"
    print(t[2])
    打印出来的,追踪了table从赋值到访问的过程
    *update of element 2 to hello
    *access to element 2
    hello


    Read-only tables
    只读table

    只读table的原理主要就是在试图给table赋值的时候做限制,这里就涉及到了__newindex 元方法的使用。
    e.g.
    --read only table
    function readOnly (t)
         local proxy = {}
         local mt = {
              __index = t,
              __newindex = function (t,k,v)
                   error("attempt to update a read-only table",2)      --在试图改变元素的时候抛出错误,且参数为2,表示在报错的地方将会是调用该方法的地方。
              end
         }
         setmetatable(proxy,mt)
         return proxy
    end
    通过在__newindex元方法里面做恰当的修改,便能将我们的table改写为只读table。


  • 相关阅读:
    整数反转
    最长公共前缀
    罗马数字转整数
    单点登录
    VMware Workstation虚拟机密钥
    Pytest 用例内部执行顺序
    判断是不是回文数
    python端口IP字符串是否合法
    python求二叉树深度
    有两个字符串类型的数字,实现一个方法将它们进行相加,并返回相加后的数值。
  • 原文地址:https://www.cnblogs.com/zhong-dev/p/4044576.html
Copyright © 2011-2022 走看看