9

Sweet Snippet 之 Lua readonly table

 3 years ago
source link: https://blog.csdn.net/tkokof1/article/details/112850563
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Sweet Snippet 之 Lua readonly table

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

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

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

简单测试一下:

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 可以得到正确结果):

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 元方法即可:

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 子元素仍然不是只读的:

local r_t = readonly({ 1, 2, 3, {} })
r_t[1] = 1 -- error
r_t[4] = {} -- error
r_t[4][1] = 1 -- correct ...

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

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 仍然会被更新(而不能做到只读):

local r_t = readonly({ 1, 2, 3, {} })
rawset(r_t, 1, 2) -- correct ...

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK