遞迴的唯讀表

lua-users home
wiki

由: VeLoSo

Lua 版本: 5.x

先備條件:熟悉元方法(請參閱 MetamethodsTutorial

本著 ReadOnlyTables 的精神,我需要一種方法在多使用者 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(唯讀表的建構函式)。

我比較偏好快取唯讀版本的各個表,以避免重複建立副本(並支援唯讀表的相等性測試),但正如 RiciLakeGarbageCollectingWeakTables 中指出的,快取遞迴資料在 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 元表來強化這個作法,並希望找出一個不會中斷垃圾收集的快取策略。也許有一天我會發一篇遞迴唯讀表二

-- 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 (diff)