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 替换了 getfenvsetfenv,允许 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)”之后,直到执行“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 正在被重新定义,它们将无法在函数内部找到。

--DirkLaurie

另请参阅


最近更改 · 偏好设置
编辑 · 历史记录
最后编辑于 2013 年 2 月 26 日上午 7:37 GMT (差异)