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

时间:2013-02-18 17:58:13

标签: lua

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)

5 个答案:

答案 0 :(得分:4)

你给master.lua两个职责:

  1. 它定义了通用模块表
  2. 导入所有子模块
  3. 相反,您应该为(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'
    ...
    

    测试现在应该通过。

答案 1 :(得分:3)

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

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

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

编辑:它非常接近安德鲁·斯塔克的第一个解决方案,除了我直接在子模块中修补MASTER表。优点是您可以同时定义多个内容,例如 simple.lua multi.lua reference.lua 文件。< / p>

答案 2 :(得分: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表的引用,从后面的代码的角度模拟全局。使用此沙箱作为环境会导致以后所有需要在此上下文中评估其“全局”代码。

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

答案 3 :(得分:1)

问题涉及:

  1. 制作模块时不会污染全局空间。
  2. 制作模块的方式可能会因为维护原因而分成多个文件。
  3. 我对上述问题的解决方案在于调整Lua中的“return as table”习惯,这样当你需要在子模块之间传递状态时,不会返回一个表,而是返回一个返回表的函数。 / p>

    这适用于完全依赖于某些根模块的子模块。如果他们 独立加载,那么他们需要用户知道他们需要调用模块才能使用它。这与其他具有方法集合的模块不同,可以从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卡路里。

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

答案 4 :(得分:1)

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

执行此操作的干净方法是显式注册模块,而不是依赖require在最后隐式注册它。文档说:

  

require (modname)

     

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

     

[...]

     

找到加载器后,require使用两个参数调用加载器:   modname和一个额外的值取决于它如何获得加载器。 (如果   loader来自一个文件,这个额外的值是文件名。)如果是loader   返回任何非零值 [例如。你的文件return是模块表] ,   require将返回的值分配给package.loaded[modname]。如果   loader没有返回非零值,并且没有分配任何值   package.loaded[modname] ,然后requiretrue分配给此条目。   在任何情况下,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"

你已经完成了。