快速掌握Lua 5.3 —— "metatables" and "metamethods" (2)

Q:如何定义访问”table”相关的”metamethods”?

A:访问”table”相关的”metamethods”有两个,__index__newindex
1、之前说过,当访问一个”table”中不存在的域时,返回结果是nil。这是正确的,但并不是完全正确。实际上当这种情况发生时,Lua会试图寻找对象的”metatable”中名为__index的”metamethod”。如果没有这个”metamethod”,那么返回nil,否则由这个__index负责返回结果。
之前在“快速掌握Lua 5.3 —— 函数”的“附加 4”中提到过一种自动设定默认值的方法。那种方法是在函数内部帮你补填好默认值,但是从你创建的”table”中无法获取函数内部提供的默认值。而现在有了__index,实现方法就更加灵活,我们可以实现创建一张相当于带有默认值的”table”,

-- create a namespace
Window = {}
-- create the prototype with default values
Window.prototype = {x=0, y=0, width=100, height=100, }
-- create a metatable
Window.mt = {}
-- 为所创建的"table"分配"metatable"。
function Window.new (o)
    setmetatable(o, Window.mt)
    return o
end
--[[ 定义"metatable"返回"Window.prototype"中存储的默认值。
     当"__index"被调用时,
     参数"table"是"w",参数"key"是"width"。]]
Window.mt.__index = function (table, key)
    return Window.prototype[key]
end

w = Window.new{x=10, y=20}
print(w.width)    --> 100
---------------------------

__index不像其他”metamethod”一样需要是个函数,它可以是一张”table”。
当他是个函数时,Lua调用它,以”table”和缺失的”key”作为参数(就像上面例子中那样)。而当他是一个”table”时,Lua直接以缺失的”key”作为它的”key”再次访问他(相当于拿着缺失的”key”在它这张”table”中寻找),所以上面的例子中定义__index的部分可以改为,
Window.mt.__index = Window.prototype
达到的效果是相同的。
2、__newindex__index的功能是互补的关系。当向一个”table”中存入之前不存在的元素时__newindex被调用(当你向”table”中存储一个之前不存在的”key-value”时,Lua首先会查找对象的”metatable”中的”__newindex”域,如果找到了则调用它,否则进行正常的存入操作)。

-- 继续上面的例子。
Window.mt.__newindex = function(t, k, v) Window.prototype[k] = v end
w["z"] = 30
print(Window.prototype.z)    --> 30
print(w.z)    --> 30

__index的特性相同,如果__newindex是一个函数,Lua以”table”,”key”,”value”作为参数调用它(就像上面例子中那样)。而如果是一个”table”,Lua在这张”table”上做正常的存入操作,所以__newindex的部分更改为,
Window.mt.__newindex = Window.prototype
是相同的效果。

Q:如何监控对”table”的操作?

A:__index__newindex均是在”table”中没有指定的”key”时起作用,如果我们想监视对”table”的所有操作,唯一的方法是将”table”一直保持为空。所以我们需要这样的一个”table”,为其分配”metatable”并设置__index__newindex,在这两个”metamethod”内部将”key-value”传递给真正的”table”,或者从真正的”table”中取出”key-value”。

t = {}    -- original table (created somewhere)
local _t = t    -- keep a private access to original table
t = {}    -- create proxy
-- create metatable
local mt = {
  __index = function (t,k)
    io.write("*access to element " .. tostring(k) .. ", ")
    return _t[k]   -- access the original table
  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'    --> *update of element 2 to hello
print(t[2])    --> *access to element 2, hello

不幸的是这种方法不支持表的遍历。当使用pairs()时,遍历的是那张代理的空表,而不是原表本身。
如果我们想监视许多的”table”,我们不需要为每个代理”table”都分配一个”metatable”。我们可以让每个代理”table”与他们对应的”table”相关连,而这些代理”table”共享一个”metatable”,关联的方法是将原”table”保存在代理”table”中。如果担心域名冲突,还可以使用一个{}作为索引,

local index = {}    -- create private index
-- create metatable
local mt = {
    __index = function (t,k)
        io.write("*access to element " .. tostring(k) .. ", ")
        --[[ 这里传入的"t"是"proxy",如果还是通过"t[index]"的方式获取原"table",
             那么这个获取的过程还是会被监视,又会调用"__index",进入无限循环。
             所以获取原"table"的时候需要绕过"__index"。
             下面"__newindex"中同理。]]
        return rawget(t, index)[k]    -- access the original table
    end,
    __newindex = function (t,k,v)
        print("*update of element " .. tostring(k) ..
        " to " .. tostring(v))
        rawget(t, index)[k] = v    -- update original table
    end
}

function track (t)
    local proxy = {}
    proxy[index] = t    -- 将原"table"存储在代理"table"中,以"{}"为"key"。
    setmetatable(proxy, mt)
    return proxy
end

table = track{}
table[2] = 'hello'    --> *update of element 2 to hello
print(table[2])    --> *access to element 2, hello

Q:如何实现只读”table”?

A:使用代理”table”的概念很容易实现只读”table”,我们只需要监测到更新”table”的操作时报错。如果我们不需要监测取数据操作,我们可以将__index指定为原”table”,这样将更有效率。

function readOnly (t)
    local proxy = {}
    local mt = { 
        --[[ 因为操作的是代理"table",其中没有任何数据,
             所以取数据还是要去原"table"。]]
        __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"    --> attempt to update a read-only table

附加:

1、如果一个”table”的”metatable”设定了__index__newindex,而我们在向”table”中存入”key-value”以及从”table”中取出”key-value”时不想触发__index__newindex,使用rawset()rawget()可以绕过他们的操作,

table = {}
new_value = {}
setmetatable(table, {__newindex = new_value, __index = new_value})

table["x"] = 90
print(rawget(table, "x"))    --> nil
print(new_value.x)    --> 90

rawset(table, "y", 10)
print(rawget(table, "y"))    --> 10
print(new_value.y)    --> nil

2、有了__index,将一个”table”中未初始化元素的默认值由nil更改为0也就非常的简单了,

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

这段程序为每个需要默认值的”table”创建了一个”metatable”,注意,是在setDefault()内部创建的,也就是说每个需要默认值的”table”都有一个自己独有的”metatable”,这样对于有许多需要默认值的”table”来说开销会非常大(那得有很多单独的”metatable”被创建)。
显然默认值是与”table”相关连的,这样我们其实可以将默认值存储在他们对应的”table”中(比如default域,不过这样可能造成域名冲突,如果你不想让这个默认值域与其他的域发生有可能的冲突,你可以使用一个特殊的域名,比如___。如果这样你依旧不放心的话,你可以像下面的程序那样,使用一个”table”作为”key”),然后让所有”table”共享一个”metatable”,这个”metatable”中有公用的返回默认值的方法。于是程序更改如下,

local key = {}    -- unique key
local mt = {__index = function (t) return t[key] end}
function setDefault (t, d)
  t[key] = d
  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
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值