With 语句

lua-users home
wiki

摘要

在许多面向对象语言中都实现了 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 扩展对象作用域

另一种解决方案是使用 _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?

Lua 5.2

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'
--DavidManura

动态作用域方法

与其使用 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 被重新定义了。

--DirkLaurie

另请参阅


RecentChanges · preferences
编辑 · 历史
最后编辑于 2013 年 2 月 26 日上午 1:37 GMT (diff)