zoukankan      html  css  js  c++  java
  • Step By Step(Lua元表与元方法)

    Step By Step(Lua元表与元方法)

        Lua中提供的元表是用于帮助Lua数据变量完成某些非预定义功能的个性化行为,如两个table的相加。假设a和b都是table,通过元表可以定义如何计算表达式a+b。当Lua试图将两个table相加时,它会先检查两者之一是否有元表,然后检查该元表中是否存在__add字段,如果有,就调用该字段对应的值。这个值就是所谓的“元方法”,这个函数用于计算table的和。
        Lua中每个值都有一个元表。table和userdata可以有各自独立的元表,而其它数据类型的值则共享其类型所属的单一元表。缺省情况下,table在创建时没有元表,如:
        t = {}
        print(getmetatable(t))  --输出为nil
        这里我们可以使用setmetatable函数来设置或修改任何table的元表。
        t1 = {}
        setmetatable(t,t1)
        assert(getmetatable(t) == t1)
        任何table都可以作为任何值的元表,而一组相关的table也可以共享一个通用的元表,此元表将描述了它们共同的行为。一个table甚至可以作为它自己的元表,用于描述其特有的行为。在Lua代码中,只能设置table的元表,若要设置其它类型值的元表,则必须通过C代码来完成。

        1. 算术类的元方法:
        在下面的示例代码中,将用table来表示集合,并且有一些函数用来计算集合的并集和交集等。

     
     1 Set = {}
    2 local metatable = {} --元表
    3
    4 --根据参数列表中的值创建一个新的集合
    5 function Set.new(l)
    6 local set = {}
    7 --将所有由该方法创建的集合的元表都指定到metatable
    8 setmetatable(set,metatable)
    9 for _, v in ipairs(l) do
    10 set[v] = true
    11 end
    12 return set
    13 end
    14
    15 --取两个集合并集的函数
    16 function Set.union(a,b)
    17 local res = Set.new{}
    18 for k in pairs(a) do
    19 res[k] = true
    20 end
    21 for k in pairs(b) do
    22 res[k] = true
    23 end
    24 return res
    25 end
    26
    27 --取两个集合交集的函数
    28 function Set.intersection(a,b)
    29 local res = Set.new{}
    30 for k in pairs(a) do
    31 res[k] = b[k]
    32 end
    33 return res
    34 end
    35
    36 function Set.tostring(set)
    37 local l = {}
    38 for e in pairs(set) do
    39 l[#l + 1] = e
    40 end
    41 return "{" .. table.concat(l,", ") .. "}";
    42 end
    43
    44 function Set.print(s)
    45 print(Set.tostring(s))
    46 end
    47
    48 --最后将元方法加入到元表中,这样当两个由Set.new方法创建出来的集合进行
    49 --加运算时,将被重定向到Set.union方法,乘法运算将被重定向到Set.intersection
    50 metatable.__add = Set.union
    51 metatable.__mul = Set.intersection
    52
    53 --下面为测试代码
    54 s1 = Set.new{10,20,30,50}
    55 s2 = Set.new{30,1}
    56 s3 = s1 + s2
    57 Set.print(s3)
    58 Set.print(s3 * s1)
    59
    60 --输出结果为:
    61 --{1, 30, 10, 50, 20}
    62 --{30, 10, 50, 20}
     

        在元表中,每种算术操作符都有对应的字段名,除了上述的__add(加法)__mul(乘法)外,还有__sub(减法)__div(除法)__unm(相反数)__mod(取模)__pow(乘幂)。此外,还可以定义__concat字段,用于描述连接操作符的行为。
        对于上面的示例代码,我们在算术运算符的两侧均使用了table类型的操作数。那么如果为s1 = s1 + 8,Lua是否还能正常工作呢?答案是肯定的,因为Lua定位元表的步骤为,如果第一个值有元表,且存在__add字段,那么Lua将以这个字段为元方法,否则会再去查看第二个值否是有元表且包含__add字段,如果有则以此字段为元方法。最后,如果两个值均不存在元方法,Lua就引发一个错误。然而对于上例中的Set.union函数,如果执行s1 = s1 + 8将会引发一个错误,因为8不是table对象,不能基于它执行pairs方法调用。为了得到更准确的错误信息,我们需要给Set.union函数做如下的修改,如:

     
    1 function Set.union(a,b)
    2 if getmetatable(a) ~= metatable or getmetatable(b) ~= metatable then
    3 error("attempt to 'add' a set with a non-set value")
    4 end
    5 --后面的代码与上例相同。
    6 ... ...
    7 end
     


        2. 关系类的元方法:
        元表还可以指定关系操作符的含义,元方法分别为__eq(等于)、__lt(小于)和__le(小于等于),至于另外3个关系操作符,Lua没有提供相关的元方法,可以通过前面3个关系运算符的取反获得。见如下示例:

     
     1 Set = {}
    2 local metatable = {}
    3
    4 function Set.new(l)
    5 local set = {}
    6 setmetatable(set,metatable)
    7 for _, v in ipairs(l) do
    8 set[v] = true
    9 end
    10 return set
    11 end
    12
    13 metatable.__le = function(a,b)
    14 for k in pairs(a) do
    15 if not b[k] then return false end
    16 end
    17 return true
    18 end
    19 metatable.__lt = function(a,b) return a <= b and not (b <= a) end
    20 metatable.__eq = function(a,b) return a <= b and b <= a end
    21
    22 --下面是测试代码:
    23 s1 = Set.new{2,4}
    24 s2 = Set.new{4,10,2}
    25 print(s1 <= s2) --true
    26 print(s1 < s2) --true
    27 print(s1 >= s1) --true
    28 print(s1 > s1) --false
     

        与算术类的元方法不同,关系类的元方法不能应用于混合的类型。

        3. 库定义的元方法:
        除了上述基于操作符的元方法外,Lua还提供了一些针对框架的元方法,如print函数总是调用tostring来格式化其输出。如果当前对象存在__tostring元方法时,tostring将用该元方法的返回值作为自己的返回值,如:

     
     1 Set = {}
    2 local metatable = {}
    3
    4 function Set.new(l)
    5 local set = {}
    6 setmetatable(set,metatable)
    7 for _, v in ipairs(l) do
    8 set[v] = true
    9 end
    10 return set
    11 end
    12
    13 function Set.tostring(set)
    14 local l = {}
    15 for e in pairs(set) do
    16 l[#l + 1] = e
    17 end
    18 return "{" .. table.concat(l,", ") .. "}";
    19 end
    20
    21 metatable.__tostring = Set.tostring
    22
    23
    24 --下面是测试代码:
    25 s1 = Set.new{4,5,10}
    26 print(s1) --{5,10,4}
     

        函数setmetatable和getmetatable也会用到元表中的一个字段(__metatable),用于保护元表,如:

    1 mt.__metatable = "not your business"
    2 s1 = Set.new{}
    3 print(getmetatable(s1)) --此时将打印"not your business"
    4 setmetatable(s1,{}) --此时将输出错误信息:"cannot change protected metatable"

        从上述代码的输出结果即可看出,一旦设置了__metatable字段,getmetatable就会返回这个字段的值,而setmetatable将引发一个错误。

        4. table访问的元方法:
        算术类和关系类运算符的元方法都为各种错误情况定义了行为,它们不会改变语言的常规行为。但是Lua还提供了一种可以改变table行为的方法。有两种可以改变的table行为:查询table及修改table中不存在的字段。
        
        1). __index元方法:
        当访问table中不存在的字段时,得到的结果为nil。如果我们为该table定义了元方法__index,那个访问的结果将由该方法决定。见如下示例代码:

     
     1 Window = {} 
    2 Window.prototype = {x = 0, y = 0, width = 100, height = 100}
    3 Window.mt = {} --Window的元表
    4
    5 function Window.new(o)
    6 setmetatable(o,Window.mt)
    7 return o
    8 end
    9
    10 --将Window的元方法__index指向一个匿名函数
    11 --匿名函数的参数table和key取自于table.key。
    12 Window.mt.__index = function(table,key) return Window.prototype[key] end
    13
    14 --下面是测试代码:
    15 w = Window.new{x = 10, y = 20}
    16 print(w.width) --输出100
    17 print(w.width1) --由于Window.prototype变量中也不存在该字段,因此返回nil。
     

        最后,Lua为__index元方法提供了一种更为简洁的表示方式,如:Window.mt.__index = Window.prototype。该方法等价于上例中的匿名函数表示方法。相比而言,这种简洁的方法执行效率更高,但是函数的方法扩展性更强。
        如果想在访问table时禁用__index元方法,可以通过函数rawget(table,key)完成。通过该方法并不会加速table的访问效率。

        2). __newindex元方法:
        和__index不同的是,该元方法用于不存在键的赋值,而前者则用于访问。当对一个table中不存在的索引赋值时,解释器就会查找__newindex元方法。如果有就调用它,而不是直接赋值。如果这个元方法指向一个table,Lua将对此table赋值,而不是对原有的table赋值。此外,和__index一样,Lua也同样提供了避开元方法而直接操作当前table的函数rawset(table,key,value),其功能类似于rawget(table,key)。

        3). 具有默认值的table:
        缺省情况下,table的字段默认值为nil。但是我们可以通过元表修改这个默认值,如:

     
    1 function setDefault(table,default)
    2 local mt = {__index = function() return default end }
    3 setmetatable(table,mt)
    4 end
    5 tab = {x = 10, y = 20}
    6 print(tab.x,tab.z) --10 nil
    7 setDefault(tab,0)
    8 print(tab.x,tab.z) --10 0
     


        4). 跟踪table的访问:
        __index和__newindex都是在table中没有所需访问的index时才发挥作用的。因此,为了监控某个table的访问状况,我们可以为其提供一个空table作为代理,之后再将__index和__newindex元方法重定向到原来的table上,见如下代码:

     
     1 t = {}        --原来的table
    2 local _t = t --保持对原有table的私有访问。
    3 t = {} --创建代理
    4 --创建元表
    5 local mt = {
    6 __index = function(table,key)
    7 print("access to element " .. tostring(key))
    8 return _t[key] --通过访问原来的表返回字段值
    9 end,
    10
    11 __newindex = function(table,key,value)
    12 print("update of element " .. tostring(key) .. " to " .. tostring(value))
    13 _t[key] = value --更新原来的table
    14 end
    15 }
    16 setmetatable(t,mt)
    17
    18 t[2] = "hello"
    19 print(t[2])
    20
    21 --输出结果为
    22 --update of element 2 to hello
    23 --access to element 2
    24 --hello
     


        5). 只读的table:
        通过代理的概念,可以很容易的实现只读table。只需跟踪所有对table的更新操作,并引发一个错误即可,见如下示例代码:

     
     1 function readOnly(t)
    2 local proxy = {}
    3 local mt = {
    4 __index = t,
    5 __newindex = function(t,k,v)
    6 error("attempt to update a read-only table")
    7 end
    8 }
    9 setmetatable(proxy,mt)
    10 return proxy
    11 end
    12
    13 days = readOnly{"Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"}
    14 print(days[1])
    15 days[2] = "Noday"
    16
    17 --输出结果为:
    18 --[[
    19 Sunday
    20 lua: d:/test.lua:6: attempt to update a read-only table
    21 stack traceback:
    22 [C]: in function 'error'
    23 d:/test.lua:6: in function <d:/test.lua:5>
    24 d:/test.lua:15: in main chunk
    25 [C]: ?
    26 ]]--
     
     
     
  • 相关阅读:
    关于js计算非等宽字体宽度的方法
    [NodeJs系列]聊一聊BOM
    Vue.js路由管理器 Vue Router
    vue 实践技巧合集
    微任务、宏任务与Event-Loop
    事件循环(EventLoop)的学习总结
    Cookie、Session和LocalStorage
    MySQL 树形结构 根据指定节点 获取其所在全路径节点序列
    MySQL 树形结构 根据指定节点 获取其所有父节点序列
    MySQL 创建函数报错 This function has none of DETERMINISTIC, NO SQL, or READS SQL DATA in its declaration and binary logging is enabled (you *might* want to use the less safe log_bin_trust_function_creators
  • 原文地址:https://www.cnblogs.com/xiao-xue-di/p/13029276.html
Copyright © 2011-2022 走看看