With 敘述 |
|
在許多物件導向語言中,都實作了 with 敘述。
像「with ... as」的結構,若將範圍指定給暫時變數,並在需要時使用該變數,可得到簡易的解決方案。
「with」用於在暗中延伸範圍時,情況較為複雜,就像下列範例:
with (obj) { some_method(); }
Lua 中基於設計而未提供此類結構。本頁面介紹了一項簡易的解決方案。
lua 中的基本函式庫提供足夠的元件,得以實作類似「with」敘述的功能。
有兩個函式可用於操作環境:setfenv() 和 getfenv(),以及一個表格 _G 存在。
一般認知中的「with」結構,會用提供的物件延伸範圍。透過元資料操作(請參閱 getmetatable() 和 setmetatable()),才能做到這一點。
讓我們看看如何在 Lua 中獲得這項功能。
主體結構可使用下列函式實作
function with(env) local oldenv = getfenv(2); setfenv(2, env); return function() setfenv(2, oldenv) end; end;
傳回值是一個用於還原初始環境的函式。於是,結構的輪廓如下所示
local endwith = with (env)
...
any_method();
...
endwith();
此方法的主要缺點在於,我們無法存取初始範圍中的變數。有兩個簡單的方式可克服這個問題。
слегка修改過的函式
function with(env) local oldenv = getfenv(2); setfenv(2, env); return function() setfenv(2, oldenv) end, _G; end;
現在可以使用全域範圍了
local endwith, _G = with (env) ... any_method(); ... _G.print("a function from a global scope"); ... endwith();
另一個解決方案是使用 _G 延伸指定範圍
function with(env) local oldenv = getfenv(2); local mt = getmetatable(env) or {}; mt.__index = _G; setmetatable(env, mt); setfenv(2, env); return function() setfenv(2, oldenv) end, _G; end;
在此,可以省去第二個傳回值。
全域範圍可隱式取得,就像在其他程式語言一樣
local endwith = with (env) ... any_method(); ... print("a function from a global scope"); ... endwith();
最後的測試程式碼
-- tiny environment with the only function Test = { output = function() print("\tTest.output()") end }; -- function for environment test function output() print("Top-level output()") end; -- the tricky with function function with(env) local oldenv = getfenv(2); local mt = getmetatable(env) or {}; mt.__index = _G; setmetatable(env, mt); setfenv(2, env); return function() setfenv(2, oldenv) end, _G; end; function main() output(); --[[ *** local function output() print("*** the substituted function!"); end; --]] local endwith, _G = with(Test); --[[ global environment still in _G table ]] _G.print("\texplicit print() invocation"); --[[ implicit invocation ]] print("\timplicit print() invocation"); --[[ call output here ]] output(); endwith(); --[[ environment restored outside of "with" ]] output(); end; main();
你可以取消標示有「***」符號的函式註解,以娛己。它揭示了一個必須牢記的限制。
--IgorBogomazov?
LuaFiveTwo 用 _ENV
取代 getfenv
和 setfenv
,可按下列方式實作 with
。
function with(...) local envs = {...} local f = (type(envs[#envs]) == 'function') and table.remove(envs) local env if #envs == 1 then env = envs[1] else local mt = {} function mt.__index(t, k) for i=1,#envs do local v = rawget(envs[i], k) if v ~= nil then return v end end end env = setmetatable({}, mt) end if f then return f(env) else return env end end -- test local function print2(...) print('printing', ...) end print 'one' with({print=print2}, _ENV, function(_ENV) print('two', math.sqrt(4)) end) print 'three' do local _ENV = with({print=print2}, _ENV) print('four', math.sqrt(4)) end print 'five'
與使用 do...end 程式區塊來限制「with」敘述的範圍(這會進行詞彙範圍),可以明確啟用或關閉範圍,就像下列範例:
with(math,string,table) print("sin(1) = "..sin(1)) --> 0.8414709848079 print(format("The answer is %d",42)) --> The answer is 42 print(concat({"with","table","library"}," ")) --> with table library without(string) print(pcall(format,"The answer is %d",42)) --> false attempt to call a nil value
這種「with」敘述的工作方式,乃是連接 _ENV、math、string 和 table 的元表格 __index 欄位。程式碼如下。
with = function(...) local ENV = _ENV local mt = getmetatable(ENV) for k=1,select('#',...) do local tbl=select(k,...) local tblmt = getmetatable(tbl) if not mt then setmetatable(ENV,{__index=tbl}) elseif not tblmt then setmetatable(tbl,{__index=mt.__index}); mt.__index=tbl; elseif tbl~=mt.__index then error("bad argument to 'with': metatable already in use") end ENV, mt = tbl, tblmt end end
出現在同一個「with」敘述的參數,會以遞減優先順序插入。當在 _ENV 中找不到「concat」時,就會搜尋 math;在 math 中找不到,則搜尋 string;在 string 中找不到,則搜尋 table。
不過,最近的「with」敘述優先於所有之前的敘述。
請注意,由於元方法的「遞迴尋找」特性,_ENV 本身會永遠被第一個搜尋到。
「without」陳述式只是在鍊中尋找表格,移除它,然後重新合併鍊的其餘部分。
without = function(...) for k=1,select('#',...) do local mt = getmetatable(_ENV) if mt==nil then return end local tbl=select(k,...) local tblmt = getmetatable(tbl) while mt do local index = mt.__index if index == nil then mt=nil elseif index == tbl then mt.__index = (tblmt and tblmt.__index) or nil; mt=nil else mt=getmetatable(index) end end end end
這種形式的「with」的一個副作用是,它暗含了一個物件階層。例如,在「with(math,string,table)」後,將會辨識出「math.sort」,直到執行「without(table)」為止。
也可以將表格直接新增到已經在內的任何表格下方,或僅在表格優先順序低於另一表格時才從「with」鍊中移除表格,如下所示:
do local with, without = with, without with_this = function(_ENV,...) with(...) end without_this = function(_ENV,...) without(...) end end with_this(table,string) -- string comes below table without_this(table,string) -- string is disabled only if it -- is below table
為「with」和「without」建立向上的值很重要,否則它們在函數內部無法被找到,因為 _ENV 正在被重新定義。