Lua 模块函数批判

lua-users home
wiki

本文提出的论点是,Lua 5.1 的 `module` 函数 [1] 在设计上存在缺陷,会导致在模块设计中出现不良实践,并可能通过全局变量的副作用导致代码错误和歧义,因此应该避免使用该函数。希望本文能进一步阻止使用 `module` 函数,并希望该函数在 Lua 的未来版本中被移除或改进。

(我们承认,有些人支持这种观点,也有些人反对,还有些人无所谓——例如,在主题 [15] 中。)

在详细说明 `module` 函数的弊端之前,我们注意到,是否使用 `module` 函数不仅仅是个人选择,它还会影响其他作者。Lua 模块作者很容易避免编写 `module` 调用。事实上,定义模块永远不需要该函数,因为它只是一个简单的辅助函数,它包装了常见的行为,而这些行为本身既不需要 Lua,也不需要 Lua 5.1 模块系统中其他更有用的部分,例如 `require`。[*A] 但是,由于模块通常使用其他作者编写的模块,而这些模块本身可能使用了 `module` 函数,并且 `module` 函数会导致全局副作用,因此它的影响无法通过选择完全避免,也无法在不修改这些其他模块的实现的情况下避免。在实践中,`module` 函数的使用相当普遍,这可能是因为 `module` 函数包含在 Lua 标准库中,大概是作为一种方便和标准化的模块定义最佳实践,并且许多官方或信誉良好的 Lua 资源,例如 Lua 参考手册 [2] 和 Lua 编程(PiL)[3] 鼓励使用 `module` 函数,甚至建议它是一个好函数。因此,新用户很快就会习惯使用 `module` 函数。

使用 `module` 函数定义模块的通常方法如下

-- hello/world.lua
module(..., package.seeall)
local function test(n) print(n) end
function test1() test(123) end
function test2() test1(); test1() end

它的使用方法如下

require "hello.world"
require "anothermodule"
hello.world.test2()

对 `module` 函数有两个主要的抱怨,如果 `anothermodule` 的定义如下,就会看到这两个抱怨

-- anothermodule.lua
module(..., package.seeall)
assert(hello.world.hello.world.print == _G.print)  -- weird
assert(hello ~= nil) -- where'd this come from anyway?

首先,可以通过索引模块表访问全局命名空间;其次,即使模块没有显式请求,hello 也在这个模块中可见。

第一个问题并非与 module 函数本身有关,而是由于 package.seeall 选项导致的。package.seeall 允许模块访问全局变量,而这些变量通常是隐藏的,因为 module 函数会用局部环境替换模块的当前环境。package.seeall 的作用是修改模块环境的元表,使其回退到 _G。这不仅允许模块本身访问 _G,而且 _G 中的变量也成为模块接口的一部分。在各种情况下,通过模块表暴露全局环境的行为可能对沙盒环境有害(参见 沙盒),这些变量可能会被意外使用,但更明显的是,这本身就很奇怪。

幸运的是,package.seeall 只是一个便利选项,可以避免使用。

-- hello/world.lua
local _G = _G
module(...)
function test() _G.print(123) end

或者

-- hello/world.lua
local print = print
module(...)
function test() print(123) end

这些方法有点笨拙,但可能还有其他更语法友好的方法来避免它,例如认识到模块表和模块环境表不必相同(例如,参见 模块定义 - "具有公共/私有命名空间的模块系统")。我们不会对第一个要点进行更详细的说明。

第二个问题是,module 函数存在副作用,会创建程序员无法完全控制的全局变量。在执行 module("hello.world") 时,该函数会在全局环境(初始全局环境,而不是通过 setfenv 设置的当前环境)中创建一个名为 "hello" 的表,并将模块表存储在该表中键为 "world" 的位置。但是,如果这些变量中的任何一个已经存在(例如,其他人将它们放在那里),该函数会抛出错误,这至少提供了一定程度的安全性。模块函数的行为可以通过 LuaCompat [4] 中提供的 Lua 表示来更好地理解(真实版本在 loadlib.c 中)。

local _LOADED = package.loaded
function _G.module (modname, ...)
  local ns = _LOADED[modname]
  if type(ns) ~= "table" then
    ns = findtable (_G, modname)
    if not ns then
      error (string.format ("name conflict for module '%s'", modname))
    end
    _LOADED[modname] = ns
  end
  if not ns._NAME then
    ns._NAME = modname
    ns._M = ns
    ns._PACKAGE = gsub (modname, "[^.]*$", "")
  end
  setfenv (2, ns)
  for i, f in ipairs (arg) do
    f (ns)
  end
end

问题出现的原因是,我们有不同的模块由不同的人维护,他们都写入全局环境。此外,使用这些模块的应用程序也可能写入全局环境。由于信息隐藏,[5] 模块和应用程序不应该了解这些模块的内部工作原理/实现,也不应该了解这些模块所需的模块名称。结果是,程序无法控制哪些全局变量被设置。下面将说明由此产生的各种类型的问题。

在以下示例中,为了方便起见,我们将内联定义模块,而不是在单独的文件中定义。例如,而不是创建两个这样的文件

-- mymodule.lua
module(...)
function test() return 1+2 end

-- mymodule_test.lua
require "mymodule"
print(mymodule.test())

我们将简单地写

(function()
  module("mymodule")
  function test() return 1+2 end
end)();
print(mymodule.test())

这是第一个例子

(function()
  local require = require
  local print = print
  local module = module
  module("yourmodule");

  (function() module("mymodule") end)()

  print(mymodule ~= nil) -- prints false (where is it?)
end)();

print(mymodule ~= nil) -- prints true (where did this come from?)

如所示,加载诸如 "mymodule" 之类的模块总是会填充全局环境,而不是填充使用该模块的当前环境。这与所需的结果相反。许多这样的模块加载可能会用旨在私有的变量填充全局环境。

另一个问题是,正如 Mark Hamburg 在 [16] 中指出的那样,将模块放入全局命名空间会隐藏依赖关系。假设你的程序加载了module "bar",而加载module "bar" 也加载了module "foo"。现在,module "foo" 也将在全局命名空间中可用。在你的程序中,你开始从全局命名空间使用module "foo"。如果module "bar" 现在删除了对module "foo" 的依赖,它也将不再在全局命名空间中可用,并导致你的程序崩溃。你无法立即知道全局命名空间中的foo 来自哪里,也不知道它实际上是一个模块(曾经是module "bar" 的依赖项)。

以下两个示例相互关联

function test() return 1+2 end

(function()
  module("mymodule", package.seeall);

  (function()
    module("test.more") -- fails: name conflict for module 'test.more'
    function hello() return 1+2 end
  end)()
end)()

(function()
  module("test")
  function check() return true end
end)();

(function()
  module("test.check") -- fails: name conflict for module 'test.check'
  function hello() return 1+2 end
end)();

如你所见,包名和普通变量名会发生冲突。module 函数会检测并引发错误,如果它要覆盖的全局变量已经存在。这就是我们想要的,对吧?但是,这也意味着加载模块是否成功是不可确定的,因为模块可能会加载其他模块,而我们可能不知道这些模块的名称(及其成员的名称),并且它们与全局变量冲突。

顺便说一下,在其他一些语言(例如 Perl)中,变量和包名在不同的命名空间中维护,因此可以防止它们冲突。[*3] 值得注意的是,模块命名约定会影响名称是否以及如何发生冲突。例如,Java 包名 [6] 通常以作者控制的(唯一)域名作为前缀,这虽然冗长,但提供了一种避免冲突的机制。在 Perl 中,CPAN 提供了一个中央命名注册表来防止冲突,具有相同前缀的模块表示共同的功能而不是共同的维护者(例如,“CGI” [7] 和 “CGI::Minimal” [8] 由不同的作者独立维护,而 “CGI::Minimial” 存储在 “CGI” 表中)。

(function()
  module("mymodule", package.seeall);

  (function()
    module("test.more")
    function hello() return 1+2 end
  end)()

  function greet()
    test.more.hello()  -- fails -- attempt to index global 'test' (a function value)
  end
end)();

function test()
  mymodule.greet()
end

test()

在这里,程序无意中覆盖了由模块函数设置的全局变量。模块函数不会检测到这一点。相反,当依赖此全局变量的模块尝试访问此变量时,程序会发生故障(可能是静默的)。

(function()
  local require = require
  local module = module
  local print = print
  local _P = package.loaded
  module('yourmodule.two');

  (function()
    module('mymodule.one')
  end)()

  print(_P['mymodule.one'] ~= nil) -- prints true
end)();

local _P = package.loaded
print(_P['mymodule.one'] ~= nil) -- prints true

将模块存储在全局环境中实际上有些冗余,因为它们也存储在package.loaded中(尽管没有为模块名称中的句点创建嵌套表)。

~~~

可以通过不使用module函数,而是以以下简单方式定义模块来避免上述问题:[*1][*2]

-- hello/world.lua
local M = {}

local function test(n) print(n) end
function M.test1() test(123) end
function M.test2() M.test1(); M.test1() end

return M

并以这种方式导入模块

local MT = require "hello.world"
MT.test2()

请注意,公共函数用M.前缀明确标明。与使用module不同,全局环境虽然可以通过MT表访问(即MT.print == nil),但hello.world表并没有导出(或污染)到全局环境中,而是一个词法表,并且具有相同前缀的模块(例如hello.world.again)不会改变hello.world表。在客户端代码中,模块hello.world可以被赋予一个对该模块来说是局部的简短缩写(例如MT)。这种方法也适用于DetectingUndefinedVariables。这很棒。唯一的问题是公共函数需要在模块本身中使用M.前缀,但其他解决方案通常会引入自己的问题和复杂性,例如上面提到的package.seeall。使用M.(两个字符)明确地表示并没有什么不好,尤其是在代码规模变大时。

关于C代码的补充说明:C语言中的luaL_register [9] 函数在某种程度上类似于Lua中的module函数,因此luaL_register也存在类似的问题,至少在使用非NULL libname时是这样。此外,luaL_newmetatable/luaL_getmetatable/luaL_checkudata函数使用C字符串作为全局注册表的键。这会带来一些潜在的命名冲突,可能是因为模块是由不同的人编写的,也可能是因为它们是同时加载的同一个模块的不同版本。为了解决这个问题,可以使用一个轻量级用户数据(指向静态链接变量的指针,以确保全局唯一性)作为键,或者将元表存储为一个上值,这两种方法都更有效率,也更不容易出错。

module 函数(及其同类)可能带来的问题比它解决的问题更多。

--DavidManura

脚注

[*1](支持上述风格的人包括 RiciLakeDavidManura、在 IRC 上提到过它的人,MikePall [17][18][19],...(请在此处添加您的姓名))

[*2] 也有人建议将标准库朝这个方向发展 [20]

[*3] Perl 中的示例,其中模块和同名变量不会冲突

package One;
our $Two = 2;
package One::Two;
our $Three = 3;
package main;
print "$One::Two,$One::Two::Three" # prints 2,3

其他要点

以下许多要点来自 2011 年 10 月关于模块的讨论 [21][22]

捆绑

使用 module 函数定义的模块,有时我们可以简单地将它们连接起来(cat *.lua > bundle.lua),如果需要将它们捆绑到一个文件中 [21]。但是,这在一般情况下并不适用。

module("one", package.seeall)
require "two"  -- This fails unless you sort the modules according to their dependency graph
               -- (assuming, as is best design, it has no cycles and can be computed statically)
local function foo() print 'one.foo' end
function bar() foo() two.foo() end

module("two", package.seeall)
function foo() print 'two.foo' end  -- This overwrite a previous local

module("main", package.seeall)
require "one"
one.bar()

一个通用的解决方案,适用于使用和不使用 module 的模块,涉及使用 package.preload,如下所示

package.preload['one'] = function()
  module("one", package.seeall)
  require "two"
  local function foo() print 'one.foo' end
  function bar() foo() two.foo() end
end

package.preload['two'] = function()
  module("two", package.seeall)
  function foo() print 'two.foo' end
end

package.preload['main'] = function()
  module("main", package.seeall)
  require "one"
  one.bar()
end

require 'main'

BinToCee 底部列出的许多捆绑实用程序都利用了这种方法。

在私有和公共之间切换

对“M”表式模块定义的一种批评是,如果模块中的函数定义从公共更改为私有,那么对该函数的所有引用都必须重命名(例如,M.foo 更改为 foo[23]

function M.foo() end          -- change to "local function foo() end"
function M.bar() M.foo() end  -- and also change "M.foo()" to "foo()"

一个缓解因素是,对 M.foo() 的引用局限于当前模块,通常数量相对较少。这里所需的重构操作与您想要重命名函数时所需的重构操作相同,无论如何您都需要这样做。文本编辑器可以帮助进行这种重构,一些了解 Lua 语言的编辑器还可以非常稳健地重命名变量。在某些语言(例如 Python)中,私有变量与公共变量的区分是通过前导下划线来实现的,因此同样的批评也适用于这些语言。

避免重命名的一个技巧是将所有函数保持为局部函数,并将任何应该公开的函数在其定义后立即插入到公共表中。

local function foo() end; M.foo = foo

一些性能关键代码为了微小的性能优势而这样做。在定义中三次使用foo是不幸的,为了避免这种情况的解决方法(例如,在模块定义中的localmodule或令牌过滤器)可能不值得。

最后,请注意,将函数从公共(表或全局变量)更改为私有(局部变量)也可能需要移动函数定义。局部变量与表或全局变量不同,它们是词法作用域的,因此必须在使用之前声明(或前向声明)。不熟悉词法作用域的新用户可能会对此感到困惑。我们可以通过使用 locals(如上面的示例)或表/全局变量(如下所示)统一声明所有变量(公共和私有)来避免这种情况。后者可能涉及使用下划线作为前缀的私有变量或使用两个表的类似 Python 的技术。

local M = {}
function M._foo() print 'foo' end
function M.bar() M._foo() end
return M

local M = {} -- public
local V = {} -- private
function V.foo() print 'foo' end
function M.bar() V.foo() end
return M

但是,这些方法都没有解决将函数从公共更改为私有或从私有更改为公共时需要替换引用的问题。我们也可以通过使用类似的技术来解决这个问题。

local M = {} -- public
local V = setmetatable({}, {__index = M}) -- private and public
function V.foo() print 'foo' end
function M.bar() V.foo() end
return M

现在,我们始终可以通过仅更改文件中的一个字符来安全地将函数从公共更改为私有或从私有更改为公共。如果我们想避免一些杂乱,我们可以将其中一些内容移到模块加载器中,以便模块只需要编写为

function V.foo() print 'foo' end
function M.bar() V.foo() end

它可能不是最简洁的,但公共和私有范围(V/M)之间的区别是明确的。

机制而非策略

“尽管我们有“机制而非策略”的规则——我们发现它在指导 Lua 的演变方面很有价值——我们应该早些时候为模块和包提供一组精确的策略。缺乏构建模块和安装包的通用策略,阻止了不同群体共享代码,并阻碍了社区代码库的开发。Lua 5.1 为模块和包提供了一组策略,我们希望这将解决这种情况。”——Lua 的演变,https://lua.ac.cn/doc/hopl.pdf

“通常,Lua 不会设置策略。相反,Lua 提供了足够强大的机制,使开发人员群体能够实现最适合他们的策略。但是,这种方法不适用于模块。模块系统的主要目标之一是允许不同的群体共享代码。缺乏通用策略阻碍了这种共享。”——http://www.inf.puc-rio.br/~roberto/pil2/chapter15.pdf

最近的澄清:LuaList:2011-10/msg00485.html

另请参见 机制而非策略

模块函数使用的普遍性

存储库中大多数纯 Lua 模块当前使用module函数。

模块函数中使用 "..."

LuaList:2011-10/msg00686.html 认为在模块文本中明确指定模块名称是更好的做法,这样可以清楚地了解如何加载它。

module("foo.bar")    -- encouraged
module(...)          -- discouraged
-- module: foo.bar   -- name in informal comment better than nothing
local M = {} return M                        -- anonymous and likewise discouraged
local M = {}; M._NAME = "foo.bar"; return M  -- better than above

另一方面,这会使包难以重新定位。

Lua 版本兼容性

Lua 5.1 的 module 函数可以通过 [LuaCompat] 在 Lua 5.0 中使用。Lua 5.2.0-beta 具有兼容模式,此外,“使用调试库在 5.2 中编写一个 'module' 函数非常容易。(但它不允许在一个文件中使用多个模块,这本身就是一种 hack)”(Roberto,LuaList:2011-10/msg00488.html)。

"M" 表格样式的模块定义在 5.0、5.1 和 5.2.0-beta 中也兼容。

_ENV 在 5.1 中不受直接支持,因此使用它可能会导致模块无法与 5.1 保持兼容。您可以使用 setfenv 模拟 _ENV,并通过 __index/__newindex 元方法捕获对它的获取/设置,或者干脆避免使用 _ENV

Lua 5.2 模块定义

在 5.2.0-beta 中,您可以继续使用 "M" 表格样式的模块定义,有些人推荐使用这种方式 [24]。另一方面,有些人建议在 Lua 5.2 中,模块应该像这样编写,使用新的 _ENV 变量(它在很大程度上取代了 setfenv

_ENV = module(...)
function foo() end

有些人反对新用户需要了解看起来很晦涩的 _ENV。虽然可以通过在加载代码块时由其他人设置 _ENV 来避免这种情况(例如,通过 require 或搜索器函数),但其他人仍然认为模块的私有环境和公共表格不应该混合,并且根本不需要 _ENV

促进模块开发

LuaModuleFunctionCritiqued 中的论点是,module 存在技术缺陷(副作用和晦涩的边缘情况),这阻碍了模块化的核心属性:可组合性。在实践中,这意味着一个应用程序加载两个由不同作者编写的不同模块时,不应该出现两个模块之间任何意外的交互。另一方面,"M" 表格样式(如果使用得当)不存在这些缺陷,因为它具有非常简单的语义,没有副作用(正式地说,模块加载器通常可以被认为是函数式编程意义上的 [纯函数])。

Hisham 认为,尽管 `module` 存在技术缺陷,但这些缺陷在很大程度上可以修复,并且与它在模块定义方面推广更标准的策略(在 5.0 中不存在)的成功相比,这些缺陷微不足道。这种成功似乎是在社会学层面而不是技术层面。据说 `module` 促进了代码共享和社区代码库的开发,并且大多数 LuaRocks 中的模块都使用 `module`。此外,使用 `module`(在某些其他语言中是内置关键字)具有自我文档化的特性,用最少的样板代码宣布代码的意图(我是一个模块,这是我的名字,这里是我的公共函数)。我们似乎也都同意,模块中晦涩的样板代码(`setfenv` / `_ENV` 东西)对可读性不利。

然而,有人可能会争辩说,在 5.1 模块系统引入几年后,人们仍然经常听到关于 Lua 模块的数量、质量和一致性的抱怨,即使是像 标准库 这样的基本内容。还有其他因素在起作用,像 LuaRocks 这样的努力正在解决其中的一些问题,但更难的问题是区分 `module` 是帮助了还是伤害了,以及 5.2 中的更改是否会有所帮助。

鉴于像 penlight [10] 这样的“标准库”已从其实现中删除了 `module` 调用 [26],这似乎并没有对任何人造成负面影响。然而,Lua 5.2.0-beta 已弃用 `module` 的事实,表明使用它的现有模块可能无法正常工作,除非加载兼容性函数或重写,这引起了人们的担忧 [27]

混合全局和模块命名空间

在将全局变量(例如 `print`)和模块函数(例如 `foo.print`)放在同一个命名空间中,就像 `package.seeall` 所做的那样,模块函数可能会覆盖同名的全局变量。这是有些人更喜欢通过为模块函数提供唯一的前缀(例如 `M.print`)来明确的原因之一。这也有助于可读性,因为它很明显 `M.print` 是从当前模块导出的公共变量,而不是局部变量、全局变量或导入的包名称。有时,在代码的其他部分(例如“`self.`”或“`ClassName:`”)中也需要这种前缀。这种明确性也避免了使用元表合并两个命名空间的任何问题或开销。但是,这种做法确实引入了一些重复(参见上面的“在私有和公共之间切换”)。

在 C++ 社区中也发生过类似的争论,涉及像“`using std;`” [11] 这样的东西,它将可能冲突的名称导入到当前命名空间,因此最好避免。此外,在 C 中,有一种常见的做法是在全局变量或静态变量之前添加“`g_`”或“`s_`”(类似地,在 C++ 中为成员添加“`m_`”),尽管智能 IDE 可以减轻对这种做法的一些需求。

Python 和 Perl 的命名泄露

Python 存在类似于 package.seeall 的问题 [23]

-- bar.py
import logging # logging is now available as bar.logging

Perl 也可能存在此问题

package One;
use Carp qw(carp);  # carp is now available via One::carp
carp("test");
1

除非你避免符号导入(并使用完全限定的名称)

package One;
use Carp qw();  # carp is now available via One::carp
Carp::carp("test");
1

或者使用 [namespace::clean][namespace::autoclean],或 [namespace::sweep] 模块。

Python(类似于 Lua 的 module)在模块 foofoo.baz 之间强加了一种关系。如果你的模块加载了 foo,而另一个模块加载了 foo.baz,那么 baz 将被放置在你的模块中。这可能是 Hitchhiker's Guide to Packaging [12] 建议模块名称的第一部分(“foo.”)在全局范围内唯一的的原因,并且似乎 Python 包倾向于共享相同的前缀,前提是它们由同一个实体管理(例如,许多但并非所有 Zope 包/模块都在 “zope.” 前缀下)。相同的准则应该适用于当前状态下的 Lua。

Perl 并不完全相同,因为你可以有一个包 Foo 具有变量 $Foo::Baz,而另一个包 Foo::Bar,它们不会冲突,因为包存在于它们自己的命名空间中。然而,Perl 与 Lua 的 module 共享以下问题

# One.pm
package One; sub test { }; 1

# Two.pm
package Two; One::test(); 1

# main.pl
use One;
use Two;   # This succeeds as written but fails if the lines are later reversed

通过 package.loaded 的全局变量

Require 在 package.loaded 中全局注册模块。因此,无论模块如何定义,如果你愿意,你仍然可以全局访问已加载的模块

local L = package.loaded
.....
L['foo.bar'].baz()
L.foo.bar.baz()  -- if require'foo'.bar == require 'foo.bar'

这样做可能被认为是费力的

local FBAR = require 'foo.bar'
local FBAZ = require 'foo.baz'
local FBUZ = require 'foo.buz'
...

你可以接受它,或者你可以找到简化它的方法

-- foo.lua
return {
  bar = require 'foo.bar',
  baz = require 'foo.baz',
  buz = require 'foo.buz'
}

或者找到自动化它的方法

local _G = require 'autoload' -- under appropriate definition of autoload
_G.foo.bar.qux()
-- note: penlight offers something like this

后者没有缺少隐藏依赖项的问题,因为模块始终在需要时按需加载。另一方面,模块加载不是本地化到模块加载器函数,而是可以在模块函数使用的地方发生,这意味着错误检测可能会延迟,并且这些函数的使用可能会更容易失败(例如,由于模块未安装而导致的模块加载失败),从而使错误处理变得复杂。不过,有一些方法可以解决这个问题。

定义类

与标准化模块定义类似的一个问题是标准化类定义(面向对象编程)。此外,一些模块也是类。

例如,ratchet 的代码 [13] 做了类似的事情。

.....
module("ratchet.http.server", package.seeall)
local class = getfenv()  -- why not _M?
__index = class

function new(socket, from, handlers, send_size)
    local self = {}
    setmetatable(self, class)
    .....
    return self
end

function handle(self) ..... end

如果模块应该使用 module,那么我们应该问,这是否应该是定义类模块的方式。

lua -l switch

如果 lua -l 命令行开关没有副作用,它就没有用。在 5.1 中使用 M 样式的模块,-l 至少会在 package.loaded 中创建一个变量,但访问它很麻烦,而 -l 的目的通常是简化解释器的调用(毕竟,可以通过 -e "require....." 实现相同的效果)。在 5.2.0-beta 中,-l 即使对于 M 样式的模块,也会使用 lua_pushglobaltable 创建一个全局表。但是,通过 lua_pushglobaltable 创建的全局变量可能比预期更长,你可能希望 -e 'FB = "require 'mypackage.foo.bar'" 的效果。参见讨论 LuaList:2011-11/msg00016.html

如果 -l 用于创建全局变量,应该将其添加到 _G 中吗?或者以某种方式限制在主程序块中(例如,-lfoo 的效果是在主块的顶部添加 local foo = require 'foo'_ENV = setmetatable({foo = require 'foo'}, {__index = _G}))?后者更简洁。

也有人建议 -l 应该接受参数 [14][19]

静态分析

使用 "M" 表定义的模块,至少在没有元表的情况下,即使它们可能在形式上有所不同,也可以从第一原理(例如,词法变量和表的行为)进行静态分析,就像 LuaInspect 所做的那样 [28]

5.1 的 module 函数具有更复杂的语义(副作用和元表行为)。尽管如此,我们仍然可以在更高层次上推断含义,特别是如果遵循约定,就像 [LuaDoc] 等工具所做的那样。5.2 中的更改以及改进 module 函数的建议也可能影响这一领域。

改进“模块”

一些关于如何改进module而不是将其丢弃的建议可以在以下线程中找到:https://lua-users.lua.ac.cn/lists/lua-l/2011-10/threads.html#00481。(待办事项:将最佳建议发布在此处)

其他用户评论

不使用模块函数意味着,通过省略local关键字,很容易污染全局环境(这是不好的,这是这篇文章的目的)。因此,我们可以通过将环境更改为私有环境(可以从_G继承)并在此环境中定义_M表(如现在一样)来改进模块函数,该表将包含模块的公共接口。我也担心这些问题,并且有一种巧妙的方法可以使用模块函数而不弄乱全局环境。就是这样

package.loaded[...]={}
module(...) -- you might want to add package.seeall

但是,这不是解决通过模块访问全局命名空间的解决方案。为此,我们需要修改模块函数。希望在下一个Lua版本中实现。

--MildredKiLya

“不使用模块函数意味着,通过省略local关键字,很容易污染全局环境(这是不好的,...)”-- 没错,但可以使用DetectingUndefinedVariables中的方法在运行时之前检测到不需要的全局访问,我认为它们是应该修复的错误。

您可以按照以下步骤使用模块函数,但这会绕过模块函数的当前行为

-- mod.lua
local _E = setmetatable({}, {__index=_G})
local _M = {}
package.loaded[...] = _M
module(...)
_E.setfenv(1, _E)
function _M.test()
  return math.sqrt(9)
end
test2 = 1

--modtest.lua
local m = require "mod"
assert(not mod)
assert(m.test() == 3)
assert(not test)
assert(not test2)
assert(not m.print)
print 'done'

$ luac -p -l mod.lua | lua /usr/local/lua-5.1.3/test/globals.lua
setmetatable    1
_G      1
package 3
module  4
test2   9*
math    7
--DavidManura

另请参阅


最近更改 · 偏好设置
编辑 · 历史记录
最后编辑于 2012 年 1 月 10 日凌晨 3:05 GMT (差异)