Sweet Snippet 之 Lua readonly table

布满荆棘的人生 2023-01-08 08:28 258阅读 0赞

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

基础

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

  1. function readonly(t)
  2. local proxy = {}
  3. local mt =
  4. {
  5. __index = t,
  6. __newindex = function()
  7. error("attempt to update a readonly table", 2)
  8. end
  9. }
  10. setmetatable(proxy, mt)
  11. return proxy
  12. end

简单测试一下:

  1. local r_t = readonly({ 1, 2, 3 })
  2. print(r_t[1])
  3. -- error here : attempt to update a readonly table
  4. r_t[1] = 2

完善

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

  1. local r_t = readonly({ 1, 2, 3 })
  2. -- correct
  3. for k, v in ipairs(r_t) do
  4. print(tostring(k) .. " = " .. tostring(v))
  5. end
  6. -- error
  7. print(#r_t)
  8. -- error
  9. for k, v in pairs(r_t) do
  10. print(tostring(k) .. " = " .. tostring(v))
  11. end

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

  1. function readonly(t)
  2. local proxy = {}
  3. local mt =
  4. {
  5. __index = t,
  6. __newindex = function()
  7. error("attempt to update a readonly table", 2)
  8. end,
  9. __len = function()
  10. return #t
  11. end,
  12. __pairs = function()
  13. return next, t, nil
  14. end
  15. }
  16. setmetatable(proxy, mt)
  17. return proxy
  18. end

进阶

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

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

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

  1. local proxies = {}
  2. function readonly(t)
  3. if type(t) == "table" then
  4. local proxy = proxies[t]
  5. if not proxy then
  6. proxy = {}
  7. local mt =
  8. {
  9. __index = function(_, k)
  10. return readonly(t[k])
  11. end,
  12. __newindex = function()
  13. error("attempt to update a readonly table", 2)
  14. end,
  15. __len = function()
  16. return #t
  17. end,
  18. __pairs = function()
  19. local function readonly_next(t, i)
  20. local n_i, n_v = next(t, i)
  21. return n_i, readonly(n_v)
  22. end
  23. return readonly_next, t, nil
  24. end
  25. }
  26. setmetatable(proxy, mt)
  27. proxies[t] = proxy
  28. end
  29. return proxy
  30. else
  31. return t
  32. end
  33. end

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

问题

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

  1. local r_t = readonly({ 1, 2, 3, {} })
  2. 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?

发表评论

表情:
评论列表 (有 0 条评论,258人围观)

还没有评论,来说两句吧...

相关阅读

    相关 Sweet Snippet系列 元素删除

    1. 引子:   平时代码总会遇到一些关于集合的操作,例如添加,排序等等,都可算作稀松平常,但是集合涉及的删除操作却一直有个大坑,我自己便跳进去过好几回,在此简单一记,以自警