模块系统的一个主要目标是允许程序以不同的形式来共享代码。要实现共享就需要一些公共的规则。
Lua从5.1开始,为模块和包定义了一系列的规则。这些规则不需要语言引入额外的技能,程序员可以
使用table、函数、元表和环境来实现这些规则。有两个重要的函数可以很容易通过这些规则,它们是
require(用于使用模块)和module(用于创建模块)。程序员可以使用不同的规则来重新实现这两个函数。
从用户观点来看,一个模块就是一个程序库,可以通过require来加载。然后便得到了一个全局变量,
表示一个table。这个table就像是一个名称空间,其内容就是模块中导出的所有东西,例如函数和常量。
一个规范的模块应使require返回这个table。
使用table来实现模块的优点在于,可以像操作普通table那样来操作模块,并且能利用Lua现有的功能来
实现各种额外的功能。
15.1 require函数
要加载一个模块,只需要简单地调用require"模块名"。该调用会返回一个由模块函数组成的table,并且
还会定义一个包含该table的全局变量。然而这些行为都是由模块完成的,而非require。
Lua总是会预先加载标准库,不过也可以为标准库中的模块使用显示的require。
以下代码详细的说明了require的行为:
function require(name)
if not package.loaded[name] then -- 模块是否已加载
local loader = findloader(name)
if loader== nil then
error("unableto load module" .. name)
end
package.loaded[name] = true -- 将模块标记为已加载
local res= loader(name) -- 初始化模块
if res ~=nil then
package.loaded[name]= res
end
end
return package.loaded[name]
end
首先,它在table package.loaded中检查模块是否已加载。如果已加载,require就返回相应的值。
因此只要一个模块已加载过,后续的require调用都将返回同一个值,不会再次重复加载它。
如果模块尚未加载,require尝试为该模块找一个加载器,会先在tablepackage.preload中查询传
入的模块名。如果在其中找到了一个函数,就以该函数作为模块的加载器。通常这个table不会
找到有关指定模块的条目,那么require就会尝试从Lua文件或C程序库中加载模块。
如果require为指定模块找到了一个Lua文件,它就通过loadfile来加载该文件;而如果找到的是
一个C程序库,就通过loadlib来加载。注意,loadfile和loadlib都只是加载了代码,并没有运行它们。
为了运行代码,require会以模块名作为参数来调用这些代码。
在搜索一个文件时,require采用的路径是一连串的模式,其中每项都是一种将模块名转换为文件名
的方式。进一步说,这种路径中的每项都是一个文件名,每项中还可以包含一个可选的问号。require
会用模块名来替换这个问号,然后根据替换的结果来检查是否存在这样一个文件。路径中的每项以分
号隔开。所以require函数只处理了分号和问号。
require用于搜索Lua文件的路径存放在变量package.path中,搜索C程序库的路径存在在变量package.cpath中。
15.2 编写模块的基本方法
在Lua中创建一个模块的最简单的方法是:创建一个table,并将所有需要导出的函数放入其中,最后返回
这个table。示例:
local modname = ...
local M = {} -- 把相关方法和变量插入table M中
_G[modname] = M
package.loaded[modname] = M -- 这句赋值可以省略在模块结尾返回talbe M<如前>
如果一个模块没有返回值,require就会返回package.loaded[modname]的当前值;
15.3 使用环境
创建模块的基本方法的缺点在于:当访问同一模块中的其他公共实体时,必须限定其名称;
并且只要一个函数的状态从私有改为公有(或从公有改为私有)就必须修改其调用;
在私有声明中容易忘记关键字local。
函数环境是一种有趣的技术,它能解决所有上述创建模块时遇到的问题。基本思想就是让模块的
主程序块有一个独占的环境。这样不仅它的所有函数都可共享这个table,而且它的所有全局变量
也都记录在这个table中。还可以将所有公有函数声明为全局变量,这样它们就自动地记录在一个
独立的table中了。模块所要做的就是将这个table赋予模块名 和 package.loaded。
以下代码演示了这种技术:
local modname = ...
local M = {}
_G[modname] = M
package.loaded[modname] = M
setfenv(1, M)
此时,在声明函数或在调用同一模块内的其它函数时,也不再需要前缀。
但是,当创建了一个空table M 作为环境后,就无法访问前一个环境中的全局变量了。
常见的解决方法有:
一、最简单的方法是继承,代码如下:
local modname = ...
local M = {}
_G[modname] = M
package.loaded[modname] = M
setmetatable(M, {__index = _G})
setfenv(1, M)
必须先调用setmetatable再调用setfenv,因为通过这种方法,模块就能直接访问任何全局变量了。
二、更快捷的方法来访问其它模块,即声明一个局部变量,用以保存对旧环境的访问:
local modname = ...
local M = {}
_G[modname] = M
package.loaded[modname] = M
local _G = _G
setfenv(1, M)
此时必须在所有全局变量的名称前加"_G."。由于没有涉及到元方法,这种访问会比前一种方法略快。
三、正规的方法是将那些需要用到的函数或模块声明为局部变量:
-- 模块设置
local modname = ...
local M = {}
_G[modname] = M
package.loaded[modname] = M
-- 导入段,声明这个模块从外界所需的所有东西
local sqrt = math.sqrt
local io = io
-- 这句之后就不再需要外部访问了
setfenv(1, M)
这种技术能清晰地说明模块的依赖性,运行速度也更快;
15.4 module函数
上一节示例中的代码都以相同的模式开始:
local modname = ...
local M = {}
_G[modname] = M
package.loaded[modname] = M
setfenv(1, M)
但Lua5.1提供了一个新的函数module,它囊括了以上这些功能。在编写一个模块时可以直接用以下
代码来取代前面的设置代码 : module(...)
这句调用会创建一个新的table,并将其赋予适当的全局变量和loadedtable,最后还会将这个table
设为主程序块的环境。
默认情况下,module不提供外部访问。必须在调用它前,为需要访问的外部函数或模块声明适当的
局部变量。也可以通过继承来实现外部的访问,只需在调用module时加一个选项package.seeall。
这个选项等价于以下代码:
setmetatable(M,{__index = _G})
所以在声明一个模块时只需这么做:
module(..., package.seeall)
module在创建模块table之前,会先检查package.loaded是否已包含了这个模块,或者是否已存在与
模块同名的变量。如果module找到了这个table,就会复用该table模块。如果没有找到模块table,
module就会创建这个模块table。
15.5 子模块与包
Lua支持具有层级性的模块名,可以用一个点来分隔名称中的层级。
一个包(package)就是一个完整的模块树。它是Lua中的发行单位。
当require一个模块mod.sub时,require会用原始的模块名"mod.sub"作为key来查询table中的package.loaded
和package.preload。在搜索一个定义子模块的文件时,require会将点转换为目录分隔符,之后就像搜索普通
文件名称一样来搜索这个名称。