创建一套相互依赖的Lua文件,而不影响全局命名空间。

总结: 什么设计模式可以让你将 Lua 代码分成多个文件,并且这些文件需要共享一些信息而不影响全局表?

背景

在 Lua 中创建库时,如果要求该库影响全局命名空间,那么这被认为是不好的形式:

-- >somelib.lua<--
SomeLib = {...}

-- >usercode.lua<--
require 'somelib'
print(SomeLib) --创建全局键==糟糕

相反,将创建使用本地变量的库视为最佳实践,然后将其返回给用户以按照他们的意愿进行分配:

-- >somelib.lua<--
local SomeLib = {...}
return SomeLib

-- >usercode.lua<--
local theLib = require 'somelib' --消费者将库命名为他们希望==高效

上面的模式在使用单个文件时有效。但是,当您有多个相互引用的文件时,这将变得更加困难。

具体例子

如何重写以下一套文件,以便所有断言都可以通过?理想情况下,重写将在磁盘上保留相同的文件和每个文件的责任。(将所有代码合并到单个文件中进行重写是有效的,但不是有帮助的;)

-- >test_usage.lua<--
require 'master'

assert(MASTER.Simple)
assert(MASTER.simple)
assert(MASTER.Shared)
assert(MASTER.Shared.go1)
assert(MASTER.Shared.go2)
assert(MASTER.Simple.ref1() == MASTER.Multi1)
assert(pcall(MASTER.Simple.ref2))
assert(_G.MASTER == nil)--当前无法通过

-- >master.lua<--
MASTER = {}
require 'simple'
require 'multi'
require 'shared1'
require 'shared2'
require 'shared3'
require 'reference'

-- >simple.lua<--
MASTER.Simple = {}
function MASTER:simple() end

-- >multi.lua<--
MASTER.Multi1 = {}
MASTER.Multi2 = {}

-- >shared1.lua<--
MASTER.Shared = {}

-- >shared2.lua<--
function MASTER.Shared:go1() end

-- >shared3.lua<--
function MASTER.Shared:go2() end

-- >reference.lua<--
function MASTER.Simple:ref1() return MASTER.Multi1 end
function MASTER.Simple:ref2() MASTER:simple() end

失败:设置环境

我想通过将环境设置为具有自引用的主表来解决问题。然而,当调用像 require 这样的函数时,这不起作用,因为它们会将环境更改回去:

-- >master.lua<--
foo = "original"
local MASTER = setmetatable({foo="captured"},{__index=_G})
MASTER.MASTER = MASTER
setfenv(1,MASTER)
require 'simple'

-- >simple.lua<--
print(foo) --输出 "original"
MASTER.Simple = {} --尝试索引全局变量 'MASTER'(一个空值)
点赞
用户405017
用户405017

我们可以通过更改主文件来修改所有需要运行的代码所处的环境来解决这个问题:

--> master.lua <--
local m = {}                        -- 实际的主表
local env = getfenv(0)              -- 当前环境
local sandbox = { MASTER=m }        -- 所有要求的环境
setmetatable(sandbox,{__index=env}) -- ……还公开了真实环境的读取访问

setfenv(0,sandbox)                  -- 使用沙盒作为环境
-- 像以前一样要求所有文件
setfenv(0,env)                      -- 恢复原始环境

return m

sandbox 是一个空表,它继承了 _G 的值,但也有对 MASTER 表的引用,从后续代码的角度模拟全局。使用这个沙盒作为环境会导致所有后续的要求在这种环境中评估其“全局”代码。

我们保存真实环境以备后续恢复,这样我们就不会干扰任何可能想实际设置全局变量的后续代码。

2013-02-18 18:17:49
用户204011
用户204011

我有一个系统化的解决问题的方法。我已经在 Git 存储库中重构了您的模块,以展示它是如何工作的:https://github.com/catwell/dont-touch-global-namespace/commit/34b390fa34931464c1dc6f32a26dc4b27d5ebd69

这个想法是你应该让子部分返回一个接受主模块作为参数的函数。

如果你通过打开 master.lua 中的源文件,在开头和结尾附加标头和页脚,然后使用 loadstring,你甚至可以未经修改地使用它们(只有 master.lua 必须被修改,但它更加复杂)。个人而言,我喜欢保持显式性,这就是我在这里所做的。我不喜欢魔法?‍♀️?。

附注:这非常接近 Andrew Stark 的第一个解决方案,除了我直接在子模块中打补丁到 MASTER 表。优势在于可以一次性定义多个事物,就像您的 simple.lua_、_multi.luareference.lua 文件中一样。

2013-02-19 10:56:25
用户68664
用户68664

你正在给 master.lua 分配两个职责:

  1. 它定义了共同的模块表。
  2. 它导入了所有的子模块。

相反,你应该为 (1) 创建一个单独的模块,并在所有的子模块中导入它:

--> common.lua <--
return {}

--> master.lua <--
require 'simple'
require 'multi'
require 'shared1'
require 'shared2'
require 'shared3'
require 'reference'
return require'common' -- 返回共同的表格

--> simple.lua <--
local MASTER = require'common' -- 导入共同的表格
MASTER.Simple = {}
function MASTER:simple() end

等等。

最后,将 test_usage.lua 的第一行更改为使用本地变量:

--> test_usage.lua <--
local MASTER = require'master'
...

现在测试应该可以通过。

2013-02-19 16:38:14
用户805875
用户805875

TL;DR: 不要return模块,尽可能早地设置package.loaded[...] = your_module(仍然可以为空),然后在子模块中只需require模块即可正确共享。


干净,明确地注册模块,不依赖于require隐式注册模块。文档中说:

require(modname)

加载指定的模块。 函数首先查看 package.loaded表来确定模块是否已经加载。 如果是,则require返回存储在 'package.loaded [modname]'的值。 _[这让你获得每个文件仅运行一次的缓存行为。]_否则它会尝试为模块找到一个_loader_。 [而其中一个_searcher_正在查找要执行的Lua文件, 这会使您获得通常的文件加载行为。]

[...]

找到一个_loader_之后,require使用两个参数调用_loader_: modname和一, 个与其如何获得_loader_有关的附加值。(如果_loader_来自文件,则此附加值是文件名。)如果_loader_ 返回任何非nil值, [例如,您的文件return模块表] ,则'require' 将返回值分配给'package.loaded [modname]'。如果_loader_不返回非nil值,并且没有分配任何值 package.loaded [modname]',则'require'将此条目分配给'true'。 无论如何,require都将返回 package.loaded [modname]的最终值。

(emphasisby me. [comments])

使用“return mymodule”习语,如果您的依赖项中有循环,缓存行为将失败-缓存会更新太晚。(结果,文件可能会被加载多次(您甚至可能会得到无限循环!)并且共享将失败)。但是明确表示

local _M = { }           -- your module, however you define / name it
package.loaded[...] = _M -- recall: require calls loader( modname, something )
                 -- so `...` is `modname, something` which is shortened
                 -- to just `modname` because only one value is used

立即更新缓存,以便其他模块可以在主块return之前就require您的模块。(当然,在那时他们只能实际使用已经定义的内容。但这通常不是问题。)

package.loaded[...] = mymodule方法适用于5.1-5.3(包括LuaJIT)。


对于您的示例,您将调整master.lua的开头为

1c1,2
< MASTER = {}
---
> local MASTER = {}
> package.loaded[...] = MASTER

对于所有其他文件

0a1
> local MASTER = require "master"

然后就完成了。

2017-06-15 03:30:20