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)”之后,直到执行“without(table)”之前,才会识别“math.sort”。
也可以将表直接插入到链中,位于链中已存在的任何表下方,或者仅当表优先级低于另一个表时才从“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 正在被重新定义,它们将无法在函数内部找到。