模块定义的替代方案

lua-users home
wiki

在 Lua 中,定义“模块”[1] 有很多方法。

使用 `module` 函数

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` 中额外局部变量的原因。

localmodule

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)(...)
--PeterSchwier?,2009 年 2 月 4 日

方案 #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)

另请参阅


RecentChanges · preferences
edit · history
最后编辑于 2015 年 2 月 19 日下午 6:01 GMT (diff)