递归只读表

lua-users home
wiki

作者: VeLoSo

Lua 版本: 5.x

先决条件: 熟悉元方法(参见 元方法教程

本着 只读表 的精神,我需要一种方法来在多用户 Lua 系统中提供访问控制。值得注意的是,用户需要对复杂数据结构进行只读访问,并且不应以任何方式修改它们。

目标是即使是经验丰富的 Lua 用户也不应该能够绕过保护。

我想到的(目前大部分未经测试)解决方案如下

-- cache the metatables of all existing read-only tables,
-- so our functions can get to them, but user code can't
local metatable_cache = setmetatable({}, {__mode='k'})

local function make_getter(real_table)
  local function getter(dummy, key)
    local ans=real_table[key]
    if type(ans)=='table' and not metatable_cache[ans] then
      ans = make_read_only(ans)
    end
    return ans
  end
  return getter
end

local function setter(dummy)
  error("attempt to modify read-only table", 2)
end

local function make_pairs(real_table)
  local function pairs()
    local key, value, real_key = nil, nil, nil
    local function nexter() -- both args dummy
      key, value = next(real_table, real_key)
      real_key = key
      if type(key)=='table' and not metatable_cache[key] then
	key = make_read_only(key)
      end
      if type(value)=='table' and not metatable_cache[value] then
	value = make_read_only(value)
      end
      return key, value
    end
    return nexter -- values 2 and 3 dummy
  end
  return pairs
end

function make_read_only(t)
  local new={}
  local mt={
    __metatable = "read only table",
    __index = make_getter(t),
    __newindex = setter,
    __pairs = make_pairs(t),
    __type = "read-only table"}
  setmetatable(new, mt)
  metatable_cache[new]=mt
  return new
end

function ropairs(t)
  local mt = metatable_cache[t]
  if mt==nil then
    error("bad argument #1 to 'ropairs' (read-only table expected, got " ..
	  type(t) .. ")", 2)
  end
  return mt.__pairs()
end

__type__pairs 在每个只读表的元表中设置,以支持对标准库的相应扩展。除了元方法之外,此模块只导出 ropairs(只读表的 pairs 版本,它使用 __pairs)和 make_read_only(只读表的构造函数)。

我更希望缓存每个表的只读版本,以避免进行冗余复制(并支持只读表的相等性测试),但正如 RiciLake垃圾回收弱表 中指出的那样,在 Lua 中缓存递归数据是有问题的。幸运的是,只读表相当轻量级,所以这不像它可能的那样是一个大问题。

在我的实现中,我可能会修改 make_read_only 中的 local new={} 行以生成一个新的用户数据,以正确捕获尝试将只读表视为标准表(例如,使用 pairsipairsrawsettable.insert)。

我尽早发布此内容是为了获得一些反馈。我需要一个解决方案来解决这个问题,事实证明,用纯 Lua 编码比我预期的要容易。但任何改进都将受到欢迎。

以下是一个示例用法。

保护似乎工作正常

> tab = { one=1, two=2, sub={} }
> tab.sub[{}]={}
> rotab=make_read_only(tab)
> =rotab.two
2
> =rotab.three
nil
> rotab.two='two'
stdin:1: attempt to modify read-only table
stack traceback: ...
> rotab.sub.foo='bar'
stdin:1: attempt to modify read-only table
stack traceback: ...

不幸的是,每次访问子表都会返回一个新创建的只读表。如果一个表是只读表中的一个键,你不能从只读表中将其拉出来,但如果你通过其他方式访问它,你仍然可以使用它作为键。

> key={'Lua!'}
> rot=make_read_only {[key]=12345}
> for k,v in ropairs(rot) do print (k,v) end
table: 003DD990 12345
> for k,v in ropairs(rot) do print (k,v) end
table: 00631568 12345
> =rot[key]
12345
> for k,_ in ropairs(rot) do k[2]='Woot!' end
stdin:1: attempt to modify read-only table
stack traceback: ...

我想加强这一点,以尊重包装表的 __index__pairs 元表,并希望提出一种不会破坏垃圾回收的缓存策略。也许有一天我会发布 RecursiveReadOnlyTablesTwo?

-- VeLoSo

我看到了你的实现,并想看看我是否可以实现一些东西来解决你遇到的几个问题。你案例中的一些问题在我的案例中会导致代码中断。其中一个案例是每次访问子表都会返回一个新创建的只读表。我还担心会破坏垃圾回收,因为我想设置为只读的对象创建和销毁非常频繁。我没有像我希望的那样使用元表和这些构造的经验,所以任何建议或更正都将不胜感激。

-- recursive read-only definition

function readOnly(t)
	for x, y in pairs(t) do
		if type(x) == "table" then
			if type(y) == "table" then
				t[readOnly(x)] = readOnly[y]
			else
				t[readOnly(x)] = y
			end
		elseif type(y) == "table" then
			t[x] = readOnly(y)
		end
	end
	
	local proxy = {}
	local mt = {
		-- hide the actual table being accessed
		__metatable = "read only table", 
		__index = function(tab, k) return t[k] end,
		__pairs = function() return pairs(t) end,
		__newindex = function (t,k,v)
			error("attempt to update a read-only table", 2)
		end
	}
	setmetatable(proxy, mt)
	return proxy
end

local oldpairs = pairs
function pairs(t)
	local mt = getmetatable(t)
	if mt==nil then
		return oldpairs(t)
	elseif type(mt.__pairs) ~= "function" then
		return oldpairs(t)
	end
	
	return mt.__pairs()
end
以及一些测试。请注意,table.insert 覆盖了数据的只读副本,但不会破坏实际的数据源。有人知道如何解决这个问题吗?
> local test = {"a", "b", c = 12, {x = 1, y = 2}}
> test = readOnly(test)
> for k, v in pairs(test) do
>	print(k, v)
> end
1	a
2	b
3	table: 0x806f34a0
c	12
> =test[1]
 a
> -- anyone know how to break this one?  The code above by VeLoSo also lets this through
> table.insert(test, "blah")
> =test[1]
 blah
> test.new = 3
stdin:1: attempt to modify read-only table
stack traceback: ...
> test[3] = "something"
stdin:1: attempt to modify read-only table
stack traceback: ...
> test[3].new = "something"
stdin:1: attempt to modify read-only table
stack traceback: ...

我不确定从一个只读表格的专家那里获得的这个表格有多安全,但似乎由于表格只通过闭包间接引用,所以它应该完全安全,不会受到损坏。此外,据我所知,这不会破坏垃圾回收。请告诉我你的想法。

-- ZachDwiel?


最近更改 · 偏好设置
编辑 · 历史记录
最后编辑于 2007 年 4 月 19 日上午 8:51 GMT (差异)