5

tl;dr:什么设计模式允许您将 Lua 代码拆分到需要共享某些信息而不影响全局表的多个文件上?

背景

在 Lua 中创建库会影响全局命名空间,这种做法被认为是不好的形式:

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

--> usercode.lua <--
require 'somelib'
print(SomeLib) -- global key created == bad

相反,最好的做法是创建一个使用局部变量的库,然后将它们返回给用户以在他们认为合适的时候进行分配:

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

--> usercode.lua <--
local theLib = require 'somelib' -- consumers name lib as they wish == good

使用单个文件时,上述模式可以正常工作。但是,当您有多个相互引用的文件时,这变得相当困难。

具体例子

您如何重写以下文件套件以使断言全部通过?理想情况下,重写将在磁盘上保留相同的文件以及每个文件的职责。(通过将所有代码合并到一个文件中进行重写是有效的,但没有帮助;)

--> 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)                   -- Does not currently pass 

 

--> 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 = {} --> attempt to index global 'MASTER' (a nil value)
4

5 回答 5

4

您赋予 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' -- return the common table

--> simple.lua <--
local MASTER = require'common' -- import the common table
MASTER.Simple = {}
function MASTER:simple() end

等等

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

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

测试现在应该通过了。

于 2013-02-19T16:38:14.000 回答
3

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

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

如果你通过打开master.lua中的源文件来作弊,添加页眉和页脚并使用loadstring,你甚至可以不加修改地使用它们(只有master.lua必须修改,但它更复杂)。就个人而言,我更喜欢保持明确,这就是我在这里所做的。我不喜欢魔法:)

编辑:它非常接近 Andrew Stark 的第一个解决方案,除了我直接在子模块中修补 MASTER 表。优点是您可以一次定义多个内容,例如在您的simple.luamulti.luareference.lua文件中。

于 2013-02-19T10:56:25.610 回答
1

问题涉及:

  1. 制作模块时不污染全局空间。
  2. 出于维护等原因,以这样的方式制作模块,它们可能会被拆分为多个文件。

我对上述问题的解决方案在于调整 Lua 中的“return as table”习语,当需要在子模块之间传递状态时,您不会返回一个表,而是返回一个返回一个表的函数。

这适用于完全依赖于某些根模块的子模块。如果它们独立加载的,那么它们需要用户知道他们需要调用模块才能使用它。这与其他所有具有一组方法的模块不同,准备好从local a = require('a').

无论如何,这是这样工作的:

--callbacks.lua a -- sub-module
return function(self)
    local callbacks = {}
    callbacks.StartElement =  function(parser, elementName, attributes)
        local res = {}
            local stack = self.stack

    ---awesome stuff for about 150 lines...

    return callbacks
end

要使用它,您可以...

local make_callbacks = require'callbacks'
self.callbacks = make_callbacks(self)

或者,更好的是,在将回调表分配给父模块时,只需调用require的返回值,如下所示:

self.callbacks = require'trms.xml.callbacks'(self)

大多数情况下,我尽量不这样做。如果我在子模块之间传递状态或自我,我发现我经常做错了。我的内部政策是,如果我正在做与另一个文件高度相关的事情,我可能会没事。更有可能的是,我将某些东西放在了错误的位置,并且有一种方法可以做到这一点,而无需在模块之间传递任何东西。

我不喜欢这个的原因是我通过表传递的方法和属性在我正在处理的文件中看不到。我不能自由地重构我的一个文件的内部实现,而不会影响其他文件。所以,我谦虚地建议这个成语是一面黄旗,但可能不是一面红旗。:)

虽然这解决了没有全局变量的状态共享问题,但它并不能真正保护用户免受意外遗漏local. 如果我可以谈谈那个隐含的问题......

我要做的第一件事是从我的模块中删除对全局环境的访问。记住它只有在我不重置时才可用,重置_ENV它是我做的第一件事。这是通过仅将需要的内容打包到新_ENV表中来完成的。

_ENV = {print = print, 
    pairs = pairs, --etc
}

然而,不断地将我需要的所有东西从 lua 重新输入到每个文件中是一个巨大的、容易出错的痛苦。为避免这种情况,我在模块的基本目录中创建了一个文件,并将其用作所有模块和子模块的公共环境的主目录。我称之为_ENV.lua

注意:我不能为此使用“init.lua”或任何其他根模块,因为我需要能够从子模块加载它,这些子模块由根模块加载,根模块加载子模块模块,它们是...

我的缩写_ENV.lua文件如下所示:

--_ENV.lua
_ENV = {
    type = type,  pairs = pairs,  ipairs = ipairs,  next = next,  print =
    print,  require = require, io = io,  table = table,  string = string,        
    lxp = require"lxp", lfs = require"lfs",
    socket = require("socket"), lpeg = require'lpeg', --etc..
}
return _ENV

有了这个文件,我现在有了一个共同的工作基础。我的所有其他模块都首先加载它,使用以下命令:

 _ENV = require'root_mod._ENV' --where root_mod is the base of my module.

这个设施对我来说很重要,有两个原因。首先,它让我远离全球空间。如果我发现我在全局环境中遗漏了一些东西_G(我花了很长时间才发现我没有 tostring!),我可以回到我的_ENV.lua文件并添加它。作为必需文件,它只会加载一次,因此将其应用于我的所有子模块是 0 卡路里。

其次,我发现它为我提供了使用“将模块作为表”协议真正需要的一切,只有少数例外情况需要“返回一个返回表的函数”。

于 2013-02-19T05:12:10.593 回答
1

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

--> master.lua <--
local m = {}                        -- The actual master table
local env = getfenv(0)              -- The current environment
local sandbox = { MASTER=m }        -- Environment for all requires
setmetatable(sandbox,{__index=env}) -- ...also exposes read access to real env

setfenv(0,sandbox)                  -- Use the sandbox as the environment
-- require all files as before
setfenv(0,env)                      -- Restore the original environment

return m

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

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

于 2013-02-18T18:17:49.680 回答
1

TL;DR: 不要 return模块,package.loaded[...] = your_module尽早设置(仍然可以为空),然后只是require子模块中的模块,它将被正确共享。


做到这一点的干净方法是显式注册模块,而不是依赖于require在最后隐式注册它。文档说:

require (modname)

加载给定的模块。该函数首先查看 package.loaded表以确定是否modname已加载。如果是,则require返回存储在的值 package.loaded[modname] [这让你得到每个文件只运行一次的缓存行为。] 否则,它会尝试为模块找到一个加载器[其中一位搜索者正在寻找要运行的 Lua 文件,这可以让您获得通常的文件加载行为。]

[…]

找到require加载器后,使用两个参数调用加载器: modname以及一个取决于它如何获取加载器的额外值。(如果加载器来自一个文件,这个额外的值就是文件名。)如果加载器返回任何非零值[例如你的文件return是模块表]require则将返回的值分配给package.loaded[modname]. 如果 loader 没有返回一个非 nil 值并且没有给 赋值 package.loaded[modname],那么require赋值true给这个条目。 无论如何,require返回 的最终值 package.loaded[modname]

重点[评论]由我添加。)

使用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

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

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-15T03:30:20.550 回答