模块教程 |
|
实际上有两种创建模块的方法:5.1 和 5.2 的新方法,以及 5.0 和早期 5.1 的旧方法(已弃用)。我们先介绍新方法,然后讨论 Lua 5.0 和 5.1 的旧方法。
创建一个名为 mymodule.lua 的示例文件,内容如下:
local mymodule = {} function mymodule.foo() print("Hello World!") end return mymodule
现在,要在交互式解释器中使用这个新模块,只需执行:
> mymodule = require "mymodule" > mymodule.foo() Hello World!
在实际的脚本文件中,建议将 mymodule 变量设为局部变量:
local mymodule = require "mymodule" mymodule.foo()
但由于我们处于交互式解释器中,这会在下一行使其超出作用域,因此我们必须将其放入全局变量中。
为了避免在不同文件中重新运行模块代码而重复加载同一个模块,Lua 会将模块缓存到 package.loaded 表中。为了演示这一点,假设我们将 mymodule.lua 中的 foo 函数修改为打印 "Hello Module!"。如果我们继续在上面的同一会话中操作,将会发生以下情况:
> mymodule = require "mymodule" > mymodule.foo() Hello World!
要实际重新加载模块,我们需要先将其从缓存中删除:
> package.loaded.mymodule = nil > mymodule = require "mymodule" > mymodule.foo() Hello Module!
另一个优点是,由于它们是存储在变量中的普通表,因此模块可以任意命名。假设我们认为 "mymodule" 太长,每次想使用其中的函数时都要输入:
> m = require "mymodule" > m.foo() Hello Module!
有不同的方法可以组合一个模块表,您可以根据您的情况和个人偏好进行选择。
在顶部创建一个表,并将您的函数添加到其中:
local mymodule = {} local function private() print("in private function") end function mymodule.foo() print("Hello World!") end function mymodule.bar() private() mymodule.foo() -- need to prefix function call with module end return mymodule
使所有函数成为局部变量,并在最后将它们放入一个表中:
local function private() print("in private function") end local function foo() print("Hello World!") end local function bar() private() foo() -- do not prefix function call with module end return { foo = foo, bar = bar, }
上面两个示例的组合:
local mymodule = {} local function private() print("in private function") end local function foo() print("Hello World!") end mymodule.foo = foo local function bar() private() foo() end mymodule.bar = bar return mymodule
您甚至可以更改代码块的环境,将您创建的任何全局变量存储到模块中:
local print = print -- the new env will prevent you from seeing global variables local M = {} if setfenv then setfenv(1, M) -- for 5.1 else _ENV = M -- for 5.2 end local function private() print("in private function") end function foo() print("Hello World!") end function bar() private() foo() end return M
或者,如果您不想将所有需要的全局变量保存到局部变量中:
local M = {} do local globaltbl = _G local newenv = setmetatable({}, { __index = function (t, k) local v = M[k] if v == nil then return globaltbl[k] end return v end, __newindex = M, }) if setfenv then setfenv(1, newenv) -- for 5.1 else _ENV = newenv -- for 5.2 end end local function private() print("in private function") end function foo() print("Hello World!") end function bar() private() foo() end return M
请注意,这可能会使全局变量和模块变量的访问速度变慢,因为它会使用一个 __index 函数。之所以使用空的“代理”表而不是直接给模块一个指向 _G 的 __index 元方法以及一个 __newindex 元方法,是因为会发生以下情况:
> mymodule = require "mymodule" > mymodule.foo() Hello World! > mymodule.print("example") -- unwanted __index metamethod example
Lua 5.0 和 5.1 有一个 module 函数,其用法如下:
mymodule.lua:
module("mymodule", package.seeall) function foo() -- create it as if it's a global function print("Hello World!") end
其用法如下:
> require "mymodule" > mymodule.foo() Hello World!
它的工作原理是创建一个新的模块表,将其存储在 module 第一个参数指定的全局变量中,并将其设置为代码块的环境。因此,如果您创建了一个全局变量,它就会被存储在模块表中。
这会导致模块无法看到全局变量(例如 print)。一种解决方案是在调用 module 之前将所有需要的标准函数存储在局部变量中,但这可能很麻烦,因此解决方案是 module 的第二个参数,它应该是一个函数,该函数以模块表作为参数被调用。package.seeall 会给模块一个带有 __index 元表的元表,该 __index 指向全局表,因此模块现在可以使用全局变量。问题是,模块的用户现在可以通过模块看到全局变量。
> require "mymodule" > mymodule.foo() Hello World! > mymodule.print("example") example
这充其量是奇怪且出乎意料的,最坏情况下可能是一个安全漏洞(如果您将模块提供给一个被沙盒化的脚本)。
module 被弃用的原因,除了上述问题之外,还在于它强制为模块用户指定了一个全局名称,并且在 5.2 中,一个函数无法更改其调用者的环境(至少在没有 debug 库的情况下),这使得 module 无法实现。
5.0 及更早的 5.1 模块使用此方法,但 5.2 及新的 5.1 模块应使用新方法(返回一个表)。
如上所述,Lua 使用 package 库来管理模块。
package.path(用于 Lua 编写的模块)和 package.cpath(用于 C 编写的模块)是 Lua 查找模块的地方。它们是分号分隔的列表,每个条目都可以包含一个 ?,该 ? 会被模块名称替换。以下是它们的样子:
> =package.path ./?.lua;/usr/local/share/lua/5.1/?.lua;/usr/local/share/lua/5.1/?/init.lua;/usr/local/lib/lua/5.1/?.lua;/usr/local/lib/lua/5.1/?/init.lua;/usr/share/lua/5.1/?.lua;/usr/share/lua/5.1/?/init.lua > =package.cpath ./?.so;/usr/local/lib/lua/5.1/?.so;/usr/lib/x86_64-linux-gnu/lua/5.1/?.so;/usr/lib/lua/5.1/?.so;/usr/local/lib/lua/5.1/loadall.so
package.loaded 是一个表,已加载的模块按名称存储在此。如前所述,这充当缓存,以防止模块被加载两次。相反,require 首先尝试从该表中获取模块,如果模块为 false 或 nil,则尝试加载。
package.preload 是一个与模块名称关联的函数表。在搜索文件系统之前,require 会检查 package.preload 是否有匹配的键。如果有,则调用该函数,并将其结果用作 require 的返回值。
其他字段对于 Lua 模块的一般使用并不重要,但如果您对模块系统的运作方式感兴趣,可以在手册中找到详细描述:[1]