Sweet Snippet 之 Lua readonly table

2021-01-21 19:46:07 浏览数 (1)

Lua table 用作静态配置是常见的使用情境,而用作静态配置的 Lua table 往往都有保持只读的需求,本文简单介绍了一些让 Lua table 变更为只读的知识 (代码基于 Lua 5.4)

基础

基础变更 Lua table 为只读的方法,在 《Programming in Lua》 中就已经给出了(这里),基本思路即是通过 __index 和 __newindex 两个元方法来做 table 的读写限制,代码大体如下:

代码语言:javascript复制
function readonly(t)
    local proxy = {}
    local mt = 
    {
      __index = t,
      __newindex = function()
          error("attempt to update a readonly table", 2)
      end
    }
    setmetatable(proxy, mt)
    return proxy
end

简单测试一下:

代码语言:javascript复制
local r_t = readonly({ 1, 2, 3 })
print(r_t[1])
-- error here : attempt to update a readonly table
r_t[1] = 2
完善

上述的示例代码中,虽然我们已经让 table 变为了只读,但是获取 table 长度(#)或者使用 pairs 遍历 table 时都不能得到正确结果(使用 ipairs 可以得到正确结果):

代码语言:javascript复制
local r_t = readonly({ 1, 2, 3 })

-- correct
for k, v in ipairs(r_t) do
    print(tostring(k) .. " = " .. tostring(v))
end

-- error
print(#r_t)

-- error
for k, v in pairs(r_t) do
    print(tostring(k) .. " = " .. tostring(v))
end

完善的方法也很简单,添加相应的 __len 和 __pairs 元方法即可:

代码语言:javascript复制
function readonly(t)
    local proxy = {}
    local mt = 
    {
      __index = t,
      __newindex = function()
          error("attempt to update a readonly table", 2)
      end,
      __len = function()
          return #t
      end,
      __pairs = function()
          return next, t, nil
      end
    }
    setmetatable(proxy, mt)
    return proxy
end
进阶

上面的示例代码中仍然存在一个比较大的问题:如果 table 中存在另外的 table 元素,经过上述 readonly 函数处理之后,这些 table 子元素仍然不是只读的:

代码语言:javascript复制
local r_t = readonly({ 1, 2, 3, {} })
r_t[1] = 1 -- error
r_t[4] = {} -- error
r_t[4][1] = 1 -- correct ...

为了解决这个问题,我们需要递归的对 table 做 readonly 操作,相关代码如下:

代码语言:javascript复制
local proxies = {}

function readonly(t)
    if type(t) == "table" then
        local proxy = proxies[t]
        
        if not proxy then
            proxy = {}
            local mt = 
            {
                __index = function(_, k)
                    return readonly(t[k])
                end,
                __newindex = function()
                    error("attempt to update a readonly table", 2)
                end,
                __len = function()
                    return #t
                end,
                __pairs = function()
                    local function readonly_next(t, i)
                        local n_i, n_v = next(t, i)
                        return n_i, readonly(n_v)
                    end
                    return readonly_next, t, nil
                end
            }
            setmetatable(proxy, mt)
            proxies[t] = proxy
        end
        
        return proxy
    else
        return t
    end
end

示例代码并没有对 table 进行全量的只读变更(我们自然也可以这么做),而是在访问 table 元素时以增量方式进行的,这有益于分摊程序消耗.

问题

经过了上面几步, readonly 函数已经几近完善,但仍然存在问题,如果我们使用 rawset(类似的还有 rawget) 绕过元方法来设置 table,那么 table 仍然会被更新(而不能做到只读):

代码语言:javascript复制
local r_t = readonly({ 1, 2, 3, {} })
rawset(r_t, 1, 2) -- correct ...

如果需要解决这个问题,目前就需要在宿主语言侧(譬如 C)来实现只读的 table 类型,并导出给 Lua 来使用.

参考资料
  • Programming in Lua
  • Read Only Tables
  • Generalized Pairs And Ipairs
  • How can I implement a read-only table in lua?
lua

0 人点赞