Lua 模块函数批判 |
|
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 中的变量也成为了模块的接口。在各种情况中,通过模块表暴露全局环境的行为可能对沙箱(参见 SandBoxes)不利,并且这些变量可能会被意外使用,但更明显的是,这简直太奇怪了。
幸运的是,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
这些方式有点笨拙,但可能存在其他更符合语法的方式来避免它,例如认识到模块表和模块环境表不必是同一个表(例如,参见 ModuleDefinition -- "带公共/私有命名空间的模块系统")。我们不会深入探讨第一个问题。
第二个问题是 module 函数具有创建程序员无法完全控制命名的全局变量的副作用。执行 module("hello.world") 时,该函数会在全局环境中创建一个名为 "hello" 的表(初始的全局环境,而不是通过 setfenv 设置的当前环境),并将模块表存储在该表的键 "world" 下。然而,如果这些变量中的任何一个已经存在(例如,其他人已经放置了它们),该函数就会引发错误,这至少提供了一定程度的安全性。module 函数的行为可以通过以下 Lua 代码表示来最好地理解,该代码取自 LuaCompat [4](实际版本在 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" 的依赖,那么 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()
这里,程序无意中覆盖了 module 函数设置的全局变量。module 函数不会检测到这一点。相反,当依赖于此全局变量的模块尝试访问此变量时,程序会失败(可能是一次静默的失败)。
(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 函数(及其类似物)可能弊大于利。
[*1] (上述风格的倡导者包括 RiciLake、DavidManura、在 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 和不带 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 令人遗憾,而避免这种情况的变通方法(例如 ModuleDefinition 中的 localmodule 或令牌过滤器)可能不值得。
最后,请注意,将函数从公共(表或全局变量)更改为私有(局部变量)可能还需要移动函数定义。局部变量不像表或全局变量那样,它们是词法作用的,因此必须在使用前声明(或前向声明)。不熟悉词法作用域的新用户可能会对此感到困惑。我们可以通过统一声明所有变量(公共和私有)来避免这种情况,要么使用局部变量(如上例所示),要么使用表/全局变量(如下面将要展示的)。后者可能涉及类似 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
另请参见 MechanismNotPolicy。
module 函数的使用普遍性目前,存储库中的大多数纯 Lua 模块都使用了 module 函数。
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 5.1 的 module 函数可以通过 [LuaCompat] 在 Lua 5.0 中使用。Lua 5.2.0-beta 具有兼容模式,并且“在 5.2 中很容易编写一个‘module’函数,使用 debug 库。(但它不允许单个文件中存在多个模块,这本身就是一种 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 元方法来捕获对它的 get/set,或者干脆避免使用 _ENV。
在 5.2.0-beta 中,你可以继续使用“M”表风格的模块定义,并且有人推荐它 [24]。另一方面,有人建议在 Lua 5.2 中,模块将这样编写,使用新的 _ENV 变量(它在很大程度上取代了 setfenv):
_ENV = module(...) function foo() end
有人反对新用户需要了解晦涩难懂的 _ENV。虽然可以通过让其他人(例如 require 或搜索器函数)在加载块时设置 _ENV 来避免这一点,但其他人仍然认为模块的私有环境和公共表不应混合,并且根本不需要 _ENV。
LuaModuleFunctionCritiqued 中的论点是 module 存在技术缺陷(副作用和晦涩的边界情况),这阻碍了模块化的核心属性:可组合性。实际上,这意味着应用程序加载两个由不同作者编写的不同模块时不应遇到两个模块之间的任何意外交互。另一方面,“M”表风格(如果使用得当)没有这些缺陷,因为它具有非常简单的无副作用语义(形式上,模块加载器通常可以被视为函数式编程意义上的[纯函数])。
Hisham 认为 [25],尽管 module 存在技术缺陷,但这些缺陷在很大程度上可以修复,而且与它在促进更标准的模块定义策略(5.0 中缺失)方面的成功相比,这些缺陷微不足道。这种成功似乎是社会学层面的而非技术层面的。据称,module 促进了代码共享和社区代码库的开发,并且 LuaRocks 中的大多数模块都使用 module。此外,module 的使用(在某些其他语言中是内置关键字)具有自我文档化的属性,用最少的样板代码声明了代码的意图(我是一个模块,这是我的名称,这是我的公共函数)。我们也似乎都同意,模块中的晦涩的样板代码(setfenv/_ENV 等)不利于可读性。
然而,有人可能会争辩说,在 5.1 模块系统引入几年后,关于 Lua 模块的数量、质量和一致性的抱怨仍然时有发生,即使是像StandardLibraries 这样的基础知识也是如此。存在其他因素在起作用,像 LuaRocks 这样的项目正在解决其中一些领域,但更难的问题是区分 module 是有帮助还是有害,以及 5.2 中的变化是否有帮助。
考虑到像 penlight [10] 这样的“标准库”已经从其实现中删除了 module 调用 [26],看不出这对任何人产生了负面影响。然而,Lua 5.2.0-beta 已弃用 module,这表明使用它的现有模块可能不再有效,除非加载兼容函数或重写,这引起了一些担忧 [27]。
将全局变量(例如 print)和模块函数(例如 foo.print)放在同一个命名空间中,如 package.seeall 所做的那样,module 函数可能会覆盖同名的全局变量。这是有些人更喜欢通过给模块函数一个唯一的名称前缀(例如 M.print)来显式化操作的原因之一。这也可能有助于提高可读性,因为它清楚地表明 M.print 是从当前模块导出的公共变量,而不是局部变量、全局变量或导入的包名。有时在代码的其他部分(例如“self.”或“ClassName:”)也需要这种前缀。这种显式性也避免了通过元表合并这两个命名空间的任何问题或开销。然而,这种做法确实会引入一些重复(参见上面的“在私有和公共之间切换”)。
C++ 社区中也发生了类似的争论,例如关于“using std;”[11],它将可能冲突的名称导入当前命名空间,因此最好避免。此外,在 C 中,有一种常见的做法是在全局或静态变量前加上“g_”或“s_”(在 C++ 中对成员也类似地加上“m_”),尽管智能 IDE 可以缓解其中一些需求。
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)在模块 foo 和 foo.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
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
后者没有遗漏隐藏依赖项的问题,因为模块在需要时总是按需加载。另一方面,模块加载不局限于模块加载器函数,而是可以在模块函数使用的地方稍后发生,这意味着错误检测可能会延迟,并且这些函数的用法更容易失败(例如,由于模块未安装而导致的模块加载失败),使错误处理复杂化。不过,有方法可以解决这个问题。
一个与标准化模块定义类似的问题是标准化类定义(ObjectOrientedProgramming)。此外,一些模块也是类。
例如,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 命令行开关在没有副作用时没有用处。对于 5.1 中的 M 风格模块,-l 至少会在 package.loaded 中创建一个变量,但访问它很冗长,而 -l 的目的是通常用于快速调用解释器(毕竟,可以通过 -e "require....." 实现相同的功能)。在 5.2.0-beta 中,即使是对于 M 风格模块,-l 也会使用 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}))?后者更干净。
使用“M”表定义的模块,至少在没有元表的情况下,尽管可能在形式上有所不同,但可以从头开始进行静态分析(例如,词法变量和表的行为),如 LuaInspect 所做的那样 [28]。
5.1 的 module 函数具有更复杂的语义(副作用和元表行为)。尽管如此,我们仍然可以从更高的层面推断出意义,特别是如果遵循约定,就像 [LuaDoc] 等工具所做的那样。5.2 中的更改以及对改进 module 函数的建议也可能影响这一领域。
在 https://lua-users.lua.ac.cn/lists/lua-l/2011-10/threads.html#00481 的讨论串中有一些关于改进 module 而不是抛弃它的提议。(待办:在此处发布最佳建议)
不使用 module 函数意味着,如果省略了 local 关键字,很容易污染全局环境(这是坏的,这也是本文的目的)。因此,我们可以通过将环境更改为私有环境(可以继承自 _G)并在其中定义 _M 表(如现在)来改进 module 函数,该表将包含模块的公共接口。我也曾担心这些问题,并且有一种巧妙的方法可以使用 module 函数而不弄乱全局环境。方法如下:
package.loaded[...]={} module(...) -- you might want to add package.seeall
然而,这并不能解决通过模块访问全局命名空间的问题。为此,我们需要一个修改过的 module 函数。希望在下一个 Lua 版本中实现。
module 函数意味着,如果省略了 local 关键字,很容易污染全局环境(这是坏的,...)”——没错,但是可以通过 DetectingUndefinedVariables 中的方法在运行时之前检测到不需要的全局访问,并且我认为它们是应该被修复的错误。
module 函数的方法可以这样做,尽管这是刻意规避 module 函数的当前行为:
-- 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