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' 创建 upvalues,否则它们将在函数内部找不到,因为 _ENV 被重新定义了。