模块定义的替代方案 |
|
module(..., package.seeall) -- optionally omitting package.seeall if desired -- private local x = 1 local function baz() print 'test' end function foo() print("foo", x) end function bar() foo() baz() print "bar" end -- Example usage: require 'mymodule' mymodule.bar()
这也是一种常见且简短的方法。它使用 Lua 的 `module` 函数。在《Programming in Lua》[2] 中,还介绍了其他一些使用 `module` 函数的方法。但是,请参阅 LuaModuleFunctionCritiqued,了解对这种方法的批评。
local M = {} -- private local x = 1 local function baz() print 'test' end local function foo() print("foo", x) end M.foo = foo local function bar() foo() baz() print "bar" end M.bar = bar return M
这与表格方法类似,但即使在模块本身内部,它在引用外部变量时也使用词法变量。虽然这种代码更冗长(重复),但词法变量在性能关键代码中可能更有效,并且更适合用于DetectingUndefinedVariables 的静态分析方法。此外,这种方法可以防止对 `M` 的更改(例如来自客户端的更改)影响模块内部的行为;例如,在常规表格方法中,`M.bar()` 在内部调用 `M.foo()`,因此如果替换了 `M.foo()`,`M.bar()` 的行为将发生变化。这对 SandBoxes 也有一些影响,这也是 Lua 5.1.3 中 `etc/strict.lua` 中额外局部变量的原因。
local M = {} local x = 1 -- private local M_baz = 1 -- public local function M_foo() M_baz = M_baz + 1 print ("foo", x, M_baz) end local function M_bar() M_foo() print "bar" end require 'localmodule'.export(M) return M -- Example usage: local MM = require 'mymodule' MM.baz = 10 MM.bar() MM.foo = function() print 'hello' end MM.bar() -- Output: -- foo 1 11 -- bar -- hello -- bar
这种方法比较新颖。它使用词法(局部)变量定义模块中所有面向外部的变量。它大量使用词法变量。依赖词法变量有一些优势,例如在使用DetectingUndefinedVariables 的静态分析方法时。
`export` 函数使用 `debug` 模块读取当前函数的以 `M_` 为前缀的局部变量(`debug.getlocal`),并通过元函数通过模块表 `M` 公开它们(读/写)。能够写入这些变量是通过搜索和使用(只要可能)位于闭包中的上值(`debug.getupvalue/debug.getupvalue`)来实现的,例如在嵌套闭包中。这避免了“从表格中 - 在内部使用局部变量”中出现的重复。如果需要更动态的行为,可以有选择地将 `M_foo` 样式的引用替换为 `M.foo` 样式的引用。
`localmodule` 模块的实现假设符号没有被剥离(luac -s)并且 debug 模块没有被移除,因此这种方法确实有一些额外的负担。
`localmodule` 模块定义如下
-- localmodule.lua -- David Manura, 2008-03, Licensed under the same terms as Lua itself (MIT License). local M = {} -- Creates metatable. local getupvalue = debug.getupvalue local setupvalue = debug.setupvalue local function makemt(t) local mt = getmetatable(t) if not mt then mt = {} setmetatable(t, mt) end local varsf,varsi = {},{} function mt.__index(_,k) local a = varsf[k] if a then local _,val = getupvalue(a,varsi[k]) return val end end function mt.__newindex(_,k,v) local a = varsf[k] if a then setupvalue(a,varsi[k], v) end end return varsf,varsi end -- Makes locals in caller accessible via the table P. local function export(P) P = P or {} local varsf,varsi = makemt(P) -- For each local variable, attempt to locate an upvalue -- for it in one of the local functions. -- -- TODO: This may have corner cases. For example, we might want to -- check that these functions are lexically nested in the current -- function (possibly with something like lbci). for i=1,math.huge do local name,val = debug.getlocal(2, i) if val == nil then break end if type(val) == 'function' then local f = val for j=1,math.huge do local name,val = debug.getupvalue(f, j) if val == nil then break end if name:find("M_") == 1 then name = name:sub(3) varsf[name] = f varsi[name] = j --print('DEBUG:upvalue', name) end end end end -- For each local variable, it no upvalue was found, just -- resort to making a copy of it instead. for i=1,math.huge do local name,val = debug.getlocal(2, i) if val == nil then break end if name:find("M_") == 1 then name = name:sub(3) if not varsf[name] then rawset(P, name, val) --print('DEBUG:copy', name) end end end return P end M.export = export return M
正如《Programming in Lua》第二版第 144 页所述,当使用 Lua 5.1 模块系统和 `package.seeall` 选项(或等效的 `setmetatable(M, {__index = _G})` 技巧)时,有一个特殊之处,即全局变量可以通过模块表访问。例如,如果您有一个名为 `complex` 的模块,定义如下
-- complex.lua module("complex", package.seeall) -- ...
然后执行
require "complex" print(complex.math.sqrt(2))
打印 2 的平方根,因为 math
是一个全局变量。此外,如果一个名为 complex
的全局变量已经存在(可能在某个无关的文件中定义),那么 require
将会失败。
-- put this in the main program: complex = 123 -- then deep in some module do this: local c = require "complex" --> fails with "name conflict for module 'complex'"
这是一种命名空间污染,可能是错误的来源。
我认为问题在于模块内部使用的环境与暴露给模块客户端的表相同。我们可以将这两个表分开,如下面的解决方案所示。
-- cleanmodule.lua -- Declare module cleanly. -- Create both public and private namespaces for module. -- Global assignments inside module get placed in both -- public and private namespaces. function cleanmodule(modname) local pub = {} -- public namespace for module local priv = {} -- private namespace for module local privmt = {} privmt.__index = _G privmt.__newindex = function(priv, k, v) --print("DEBUG:add",k,v) rawset(pub, k, v) rawset(priv, k, v) end setmetatable(priv, privmt) setfenv(2, priv) package.loaded[modname] = pub end -- Require module, but store module only in -- private namespace of caller (not public namespace). function cleanrequire(name) local result = require(name) rawset(getfenv(2), name, result) return result end
示例用法
-- test.lua require "cleanmodule" m2 = 123 -- variable that happens to have same name as a module cleanrequire "m1" m1.test() assert(m1) assert(not m1.m2) -- works correctly! assert(m1.test) assert(m1.helper) assert(m2 == 123) -- works correctly! print("done")
-- m1.lua cleanmodule(...) cleanrequire "m2" function helper() print("123") end function test() helper() m2.test2() end assert(not m1) assert(test) assert(helper) assert(m2) assert(m2.test2) assert(not m2.m1) assert(not m2.m2)
-- m2.lua cleanmodule(...) function test2() print(234) end
输出
123 234 done
方案 #2 - 这是对之前代码的最新改进。此版本只替换了 module
,而不是 require
。
-- cleanmodule.lua -- Helper function added to modules defined by cleanmodule -- to support importing module symbols into client namespace. -- Usage: -- local mm = require "mymodule" -- only local exported -- require "mymodule" () -- export module table to environment -- require "mymodule" ":all" -- export also all functions -- to environment. -- require "mymodule" (target,":all") -- export instead to given table local function import(public, ...) -- Extract arguments. local target, options = ... if type(target) ~= "table" then target, options = nil, target end target = target or getfenv(2) -- Export symbols. if options == ":all" then for k,v in pairs(public) do target[k] = v end end -- Build public module tables in caller. local prevtable, prevprevtable, prevatom = target, nil, nil public._NAME:gsub("[^%.]+", function(atom) local table = rawget(prevtable, atom) if table == nil then table = {}; rawset(prevtable, atom, table) elseif type(table) ~= 'table' then error('name conflict for module ' .. public._NAME, 4) end prevatom = atom; prevprevtable = prevtable; prevtable = table end) rawset(prevprevtable, prevatom, public) return public end -- Declare module cleanly. -- Create both public and private namespaces for module. -- Global assignments inside module get placed in both -- public and private namespaces. function cleanmodule(modname) local pubmt = {__call = import} local pub = {import = import, _NAME = modname} -- public namespace for module local priv = {_PUBLIC = pub, _PRIVATE = priv, _NAME = modname} -- private namespace for module local privmt = { __index = _G, __newindex = function(priv, k, v) rawset(pub, k, v) rawset(priv, k, v) end } setmetatable(pub, pubmt) setmetatable(priv, privmt) setfenv(2, priv) pub:import(priv) package.loaded[modname] = pub end
通常以这种方式使用它
-- somemodule.lua require "cleanmodule" cleanmodule(...) local om = require "othermodule" om.hello() require "othermodule" () othermodule.hello() require "othermodule" ":all" hello()
调用者完全控制如何让被调用模块修改调用者的(私有)命名空间。
你可能会遇到的一个小问题是,当两次设置全局变量时
cleanmodule(...) local enable_spanish = true function test() print("hello") end if enable_spanish then test = function() print("hola") end end
在这里,元方法只在第一次设置时激活,因此公共命名空间将错误地包含上面定义的第一个函数。解决方法是显式地设置为 nil
cleanmodule(...) local enable_spanish = true function test() print("hello") end if enable_spanish then test = nil; test = function() print("hola") end end
(此示例最初来自 Lua设计
模式。)
--DavidManura,200703
方案 #3 - 这是对方案 #2 的进一步改进。这是一个微不足道的改变,但可能有用。我把 cleanmodule 代码放在一个匿名函数中,并调用了这个匿名函数。我还将 _G 包含在私有模块表中。这段代码可以放在任何模块文件的开头,并且不会替换任何函数。它与方案 #2 存在相同的问题,即替换值,但相同的解决方法也适用。
(function (modname) -- Helper function added to modules defined by cleanmodule -- to support importing module symbols into client namespace. -- Usage: -- local mm = require "mymodule" -- only local exported -- require "mymodule" () -- export module table to environment -- require "mymodule" ":all" -- export also all functions -- to environment. -- require "mymodule" (target,":all") -- export instead to given table local function import(public, ...) -- Extract arguments. local target, options = ... if type(target) ~= "table" then target, options = nil, target end target = target or getfenv(2) -- Export symbols. if options == ":all" then for k,v in pairs(public) do target[k] = v end end -- Build public module tables in caller. local prevtable, prevprevtable, prevatom = target, nil, nil public._NAME:gsub("[^%.]+", function(atom) local table = rawget(prevtable, atom) if table == nil then table = {}; rawset(prevtable, atom, table) elseif type(table) ~= 'table' then error('name conflict for module ' .. public._NAME, 4) end prevatom = atom; prevprevtable = prevtable; prevtable = table end) rawset(prevprevtable, prevatom, public) return public end local pubmt = {__call = import} local pub = {import = import, _NAME = modname} -- public namespace for module local priv = {_PUBLIC = pub, _PRIVATE = priv, _NAME = modname, _G = _G } -- private namespace for module local privmt = { __index = _G, __newindex = function(priv, k, v) rawset(pub, k, v) rawset(priv, k, v) end } setmetatable(pub, pubmt) setmetatable(priv, privmt) setfenv(2, priv) pub:import(priv) package.loaded[modname] = pub end)(...)
方案 #4 - 这是对方案 #2 的重构。它在 module
函数的现有框架内实现了公共/私有命名空间(不替换 module 也不替换 require)。但是,它没有解决 module
函数写入 _G 而不是写入客户端的私有环境的问题(这可能被认为是一个正交问题,可以通过重新定义 module
来解决)。
-- package/clean.lua -- -- To be used as an option to function module to expose global -- variables to the private implementation (like package.seeall) -- but not expose them through the public interface. -- -- Changes the environment to a private environment that proxies _G. -- Writes to the private environment are trapped to write to both -- the private environment and module (the module's public API). -- -- Example: -- -- -- baz.lua -- module(..., package.clean) -- function foo() print 'test' end -- function bar() foo() end -- -- Now, a client using this module -- -- require "baz" -- assert(not baz.print) -- globals not exposed (unlike package.seeall) -- baz.bar() -- ok -- -- Careful: Redefinitions will not propogate to module. Allowing that -- would require making the private environment an empty proxy table. -- -- Note: this addresses only one aspect of the problems with the module -- function. It does not addess the global namespace pollution issues. Doing -- so likely requires redefining the module function to write to the client's -- private environment rather than _G, or avoiding -- the module function entirely using a simple table approach [1]). -- -- [1] https://lua-users.lua.ac.cn/wiki/ModuleDefinition -- -- Released under the public domain. David Manura, 2009-09-14. function package.clean(module) local privenv = {_PACKAGE_CLEAN = true} setfenv(3, setmetatable(privenv, {__index=_G, __newindex=function(_,k,v) rawset(privenv,k,v); module[k]=v end} )) end return package.clean
-- package/veryclean.lua -- -- This is similar to package.clean except that the public interface is -- maintained in a separate table M, even in the private implementation. -- -- Example: -- -- -- baz.lua -- module(..., package.veryclean) -- function M.foo() print 'test' end -- function M.bar() M.foo() end -- -- This makes public methods more explicit and also simplifies -- the implementation. -- -- Released under the public domain. David Manura, 2009-09-14. function package.veryclean(module) local privenv = {M=module, _PACKAGE_VERYCLEAN = true} setfenv(3, setmetatable(privenv, {__index=_G})) end return package.veryclean
-- package/strict.lua -- -- Here's an optional replacement for strict.lua compatible with -- package.clean and package.veryclean. Example: -- -- module(..., package.veryclean, package.strict) -- -- Released under the public domain. David Manura, 2009-09-14. function package.strict(t) local privenv = getfenv(3) local top = debug.getinfo(3,'f').func local mt = getmetatable(privenv) function mt.__index(t,k) local v=_G[k] if v ~= nil then return v end error("variable '" .. k .. "' is not declared", 2) end if rawget(privenv, '_PACKAGE_CLEAN') then local old_newindex = assert(mt.__newindex) function mt.__newindex(t,k,v) if debug.getinfo(2,'f').func ~= top then error("assign to undeclared variable '" .. k .. "'", 2) end old_newindex(t,k,v) end else function mt.__newindex(t,k,v) error("assign to undeclared variable '" .. k .. "'", 2) old_newindex(t,k,v) end end end return package.strict
方案 #5 - 对方案 #4 在 package.clean() 上的重构。使用代理表来解决重新声明问题。继承 CLEAN_ENV 而不是 _G,以避免看到污染的全局环境,从而解决了 module() 引入的依赖项隐藏问题。例如,你可以在程序的最开始将 _G 的内容复制到 CLEAN_ENV 中,这样模块总是看到一个没有外部引入的依赖项的 Lua 环境。
-- kinda bloated at 4 tables and a closure per module :) local CLEAN_ENV = { pairs = pairs, unpack = unpack, ... } local P_meta = {__index = CLEAN_ENV} function package.clean(M) local P = setmetatable({}, P_meta) setfenv(3, setmetatable({}, {__index = P, __newindex = function(t,k,v) M[k]=v; P[k]=v; end})) end
--CosminApreutesei,2009 年 10 月
方案 #6 这是方案 #1 的改编版本,第一个例子,灵感来自 #4 将模块和 seeall 拆分成正交函数。在这里,我们使用一个单一的表格来表示模块命名空间,以避免双系统带来的所有同步问题。私有模块环境是一个空的代理表格,使用自定义的查找例程(_M[k] 或 _G[k],仅此而已)。私有查找中的间接寻址假设模块查找在外部比在内部更重要(您可以在内部使用局部变量)。
-- clean.lua -- Adaption of "Take #1" of cleanmodule by Ulrik Sverdrup -- My additions are in the public domain -- -- Functions: -- clean.module -- clean.require -- clean.seeall -- Declare module cleanly: -- module is registered in package.loaded, -- but not inserted in the global namespace local function _module(modname, ...) local _M = {} -- namespace for module setfenv(2, _M) -- Define for partial compatibility with module() _M._M = _M _M._NAME = modname -- FIXME: _PACKAGE -- Apply decorators to the module if ... then for _, func in ipairs({...}) do func(_M) end end package.loaded[modname] = _M end -- Called as clean.module(..., clean.seeall) -- Use a private proxy environment for the module, -- so that the module can access global variables. -- + Global assignments inside module get placed in the module -- + Lookups in the private module environment query first the module, -- then the global namespace. local function _seeall(_M) local priv = {} -- private environment for module local privmt = {} privmt.__index = function(priv, k) return _M[k] or _G[k] end privmt.__newindex = _M setmetatable(priv, privmt) setfenv(3, priv) end -- NOTE: Here I recommend a rawset version of -- https://lua-users.lua.ac.cn/wiki/SetVariablesAndTablesWithFunction -- But it is left out here for brevity. -- Require module, but store module only in -- private namespace of caller (not public namespace). local g_require = require local function _require(name) local result = g_require(name) rawset(getfenv(2), name, result) return result end -- Ironically, this module is not itself clean, so that it -- can be used with 'require' module(...) module = _module seeall = _seeall require = _require
-- Ulrik,2010 年 4 月
方案 #7 Lua 5.2 可能的模块声明
-- init.lua function module(...) local m={} for k,v in ipairs{...} do if type(v)=="table" then setmetatable(m,{__index=v}) elseif type(v)=="function" then v(m) elseif type(v)=="string" then m.notes=v end end return m end -- init-2.lua function makeenv(list,r0) local r={} for i in string.gmatch(list,"%a+") do r[i]=_G[i] end for k,v in pairs(r0) do r[k]=v end return r end function safeenv(m) return makeenv([[getmetatable assert pcall select type rawlen rawequal rawset rawget tonumber next tostring xpcall error ipairs unpack setmetatable pairs string,math,table,coroutine,bit32,_VERSION]],m) end function stdenv(m) m=safeenv(m) m=makeenv([[print loadfile require load loadstring dofile collectgarbage os io package debug]],m) return m end -- module1.lua return module("my mega module",safeenv{trace=print},function(_ENV) -- safe module. there are no load require ... even no print a=20 -- public var local b=30 -- private var function dump(x) for k,v in pairs(x) do trace(k,v) end end local function do_something() a=a+1 end -- private function end) -- module2.lua return module("some description",_G,function(_ENV) -- see all module public_var=12345 local private_var=54321 public_fn=print local private_fn=print end) -- test1.lua local m1=require "module1" m1.dump(m1)