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)」後,將會辨識出「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 正在被重新定義。

--DirkLaurie

另請參閱


RecentChanges · 偏好設定
編輯 · 歷史紀錄
上次編輯時間 2013 年 2 月 26 日 格林威治標準時間上午 7:37 (差異)