第十五课 模块与包

从用户观点来看,一个模块就是一个程序库,可以通过require来加载。然后便得到了一个全局变量,表示一个table。这个table就像是一个名称空间,其内容就是模块中导出的所有东西,如函数和常量。一个规范的模块还应使require返回这个table。
一个用于要调用一个模块中的 函数,最简单的方式是:
require "mod"
mod.foo()
如果希望使用较短的名称,则可以为模块设置一个局部名称:
local m = require "mod"
m.foo()
还可以为个别函数提供不同的名称:
require "mod"
local f = mod.foo
f()

require函数
Lua提供了一个名为require的高层函数用来加载模块,但这个函数只是假设了关于模块的基本概念。对于require而言,一个模块就是一段定义了一些值的代码。
要加载模块,只需要简单地调用require "<模块名>"。该调用会返回一个由模块函数组成的table,并且还会定义一个包含该table的全局变量。然而,这些行为都是由模块完成的,而非require。所以,有些模块会选择返回其他值,或者具有其他的效果。
即使知道某些用到的模块可能已经加载了,但只要用到require就是一个良好的编程习惯。可以将标准库排除在此规则之外,因为Lua总是会预先加载它们。
以下代码详细说明了require的行为:
function require (name)
if not package.loaded[name] then --模块是否已经加载
local loader = findloader(name)
if loader == nil then
error("unable to 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就试着为该模块找一个加载器(loader),会先在table package.preload中查询传入的模块名。如果在其中找到了一个函数,就以该函数作为该模块的加载器。通过这个preload table,就有了一种通用的方法来处理各种不同的情况。通常这个table中不会 找到有关指定模块的条目,那么require就会尝试从Lua文件或C程序库中加载模块。
如果require为指定模块找到了一个Lua文件,它就通过loadfile来加载该文件,如果找到的是一个C程序库,就通过loadlib来加载。注意,loadfile和loadlib只是加载了代码,并没有运行它们。为了运行代码,require会以模块名作为参数来调用这些代码。如果加载器有返回值,require就会将该返回值存储到table package.loaded中,以此作为将来对同一模块调用的返回值。
上述代码中还有一个重要的细节,就是在调用加载器前,require先将true赋予了package.loaded中的对应字段,以此将模块标记为已加载。这是因为如果一个模块要求加载另一个模块,而后者又要递归地加载前者。那么后者的require调用就会立即返回,从而避免了无限循环。
若要强制地使require对同一个库加载两次的话,可以简单地删除package.loaded中的模块条目。
package.loaded["foo"] = nil
require "foo"
在搜索一个文件时,require所使用的路径与传统的路径有所不同。大部分程序所使用的路径就是一连串目录,指定了 某个文件的具体位置。然而,ANSI C却没有任何关于目录的概念。所以,require采用的路径是一连串的模式,其中每项都是一种将模块名转换为文件名的方式。进一步说,这种路径中的每项都是一个文件名,每项中还可以包含一个可选的问号。require会用模块名来替换每个"?",然后根据替换的结果来检查是否存在这样一个文件。如果不存在,就会尝试下一项。路径中的每一项以分号隔开。例如,假设路径为:
?;?.lua;c:\windows\?;/usr/local/lua/?/?.lua
那么,调用require "sql"就会试着打开以下文件:
sql
sql.lua
c:\windows\sql
/usr/local/lua/sql/sql.lua
require函数只处理了分号和问号。其他例如目录分隔符或者文件扩展名,都由路径自己定义。
require用于搜索Lua文件的路径存放在变量package.path中。当Lua启动后, 便以环境变量LUA_PATH的值来初始化这个变量。如果没有找到该环境变量,则使用一个编译时定义的默认路径来初始化它。在使用LUA_PATH时,Lua会将其中所有的子串";;"替换成默认路径。例如,假设LUA_PATH为“mydir/?.lua;;”,那么最终路径就是"mydir/?.lua",并紧随默认路径。
如果require无法找到与模块名相符的Lua文件,它就会找C程序库。这类搜索会从变量package.cpath获取路径。而这个变量则是通过环境变量LUA_CPATH来初始化的。
当找到一个C程序库后,require就会通过package.loadlib来加载它。C程序库和Lua程序块是不同的,它没有定义一个单一的主函数,而是导出了几个C函数。具有良好行为的C程序库应该导出一个名为“luaopen_<模块名>”的函数。require会在链接完程序库后,尝试调用这个函数。
一般通过模块的名称来使用它们。但有时候必须将一个模块改名,以避免冲突。一种典型的情况就是,在测试中需要加载 同一模块的不同版本。对于一个Lua模块来说,其内部名称不是固定的,可以轻易的编辑它以改变其名称。但是却无法编辑一个二进制数据模块中的luaopen_*函数的名称。为了允许这种重命名,require用到一个小技巧:如果一个模块名中包含了连字符,require就会用连字符后面的内容来创建luaopen_* 函数名。例如,若一个模块名为a-b,require就认为它的open函数名为luaopen_b,而不是luaopen_a-b。

编写模块的基本方法
在Lua中创建一个模块最简单的方法就是:创建一个table,并将所有需要导出的函数放入table中,最后返回这个table。
complex = {}
function complex.new (r, i) return {r = r, i = i} end
--定义一个常量i
complex.i = complex.new(0, 1)

function complex.add (c1, c2)
return complex.new(c1.r + c2.r, c1.i + c2.i)
end

function complex.sub (c1, c2)
return complex.new(c1.r - c2.r, c1.i - c2.i)
end

function complex.mul (c1, c2)
return complex.new(c1.r * c2.r - c1.i * c2.i, c1.r * c2.i + c1.i * c2.r)
end

--将inv声明为程序块的局部变量,就是将其定义成一个私有的名称
local function inv (c)
local n = c.r^2 + c.i^2
return complex.new(c.r/n, -c.i/n)
end

function complex.div (c1, c2)
return complex.mul(c1, inv(c2))
end

return complex

上例中使用table编写模块时,没有提供与真正模块完全一致的功能性,首先,必须显示地将模块名放到每个函数定义中。其次,一个函数在调用同一个模块中的另一个函数时,必须限定被调用函数的名称。可以使用一个固定的局部名称(例如M)来定义和调用模块内的函数,然后将这个局部名称赋予模块的最终名称。
local M = {}
complex = M --模块名
function M.new (r, i) return {r = r, i = i} end
--定义一个常量i
M.i = M.new(0, 1)

function M.add (c1, c2)
return M.new(c1.r + c2.r, c1.i + c2.i)
end
<如前>
只要一个函数调用了 同一模块中的另一个函数(或者递归地调用自己),就仍需要一个前缀名称。但至少两个函数之间的连接不再需要依赖于模块名,并且也只需要在整个模块的一处写出模块名。实际上,可以完全避免写出模块名,因为require会将模块名作为参数传给模块:
local modname = ...
local M = {}
_G[modname] = M
M.i = {r = 0, i = 1}
<如前>
经过这样的修改, 若需要重命名一个模块,只需要重命名并定义它的文件就可以了。
另一项小改进与结尾的return语句有关。若能将所有与模块相关的设置任务集中在模块开头,会更好。消除return语句的一种方法是,将模块table直接赋予package.loaded:
local modname = ...
local M = {}
_G[modname] = M
package.loaded[modname] = M
<如前>
通过这样的赋值,就不需要在模块结尾返回M了。注意,如果一个模块无返回值的话,require就会返回package.loaded[modname]的当前值。

使用环境
“函数环境”是一种有趣的技术,基本想法就是让模块的主程序块有一个独占的环境。这样 不仅它的所有函数都可共享这个table,而且它的所有全局变量也都记录在这个table中。还可以将 所有公有函数声明为全局变量,这样它们就都自动地记录在一个独占的table中了。模块所要做的就是将这个table赋予模块名和package.loaded。
local modname = ...
local M = {}
_G[modname] = M
package.loaded[mdname] = M
setfenv(1, M)
此时,当声明函数add时,它就成为了complex.add:
function add (c1, c2)
return new(c1.r + c2.r, c1.i + c2.i)
end
此外在调用同一模块的其他函数时,也不再需要前缀。例如,add会从其环境中得到new,也就是complex.new。
当创建一个空table M作为环境后,就无法访问 前一个环境中全局变量了。以下提出几种重获访问的方法,每种方法都有各自的优缺点。
最简单的方法是继承:
local modname = ...
local M = {}
_G[modname] = M
package.loaded[modname] = M
setmetatable(M, {__index = _G})
setfenv(1, M)
必须先调用setmetatable再调用setfenv。这种方法导致一个后果,即从概念上讲,此时的模块中包含了所有的全局变量。例如,某人可以通过你的模块来调用标准的正弦函数:complex.math.sin(x)。
还有一种更快捷的方法访问其他模块,即声明一个局部变量, 用以保存对旧环境的访问:
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 sprt = math.sqrt
local io = io
--在这句之后就不再需要外部访问了
setfenv(1, M)
这种技术要求做更多的工作,但是它清晰地说明模块的依赖性。同时,较之前的两种方法,它的运行速度也更快。

module函数
前面的几个示例中的代码形式,它们都以相同的模式开始:
local modname = ...
local M = {}
_G[modname] = M
package.loaded[modname] = M
<setup for external access>
setfenv(1, M)
Lua5.1提供了一个新函数module,它囊括了以上这些功能。在开始编写一个模块时,可以直接用以下代码来取代前面的设置代码:
module(...)
这句调用会创建一个新的table,并将其赋予适当的全局变量和loaded table,最后还会将这个table设为主程序块的环境。
默认情况下,module不提供外部访问。必须在调用 它前,为需要访问的外部函数或模块声明适当的局部变量。也可以通过继承来实现外部访问,只需要在调用module时加上一个选项package.seeall。这个选项等价于以下代码:
setmetatable(M, {__index = _G})
因而只要这么做:
module(..., package.seeall)
在一个模块文件的开头有了这句调用后,后续所有的代码都可以像普通的Lua代码那样编写了。不需要限定模块名和外部 名字,同样也不需要返回模块table。要做的只是加上这么一句调用。
module函数还提供了一些额外的功能。虽然大部分模块不需要这些功能,但有些发行模块可能需要一些特殊处理(例如,一个模块中同时包含C函数和Lua函数)。module在创建模块table之前,会先检查package.loaded是否已经包含了这个模块,或者是否已存在与模块同名的变量。如果module由此找到了这个table,它就会复用该table作为模块。也就是说,可以用module来打开一个已创建的模块。 如果没有找到模块table,module就会创建一个模块table。然后在这个table中设置一些预定义的变量,包括:_M,包含了模块table自身(类似于_G);_NAME,包含了模块名(传给module的第一个参数);_PACKAGE,包含了包的名称。

子模块与包
Lua支持具有层级性的模块名,可以用一个点来分隔名称中的 层级。假设,一个模块名为mod.sub,那么它就是mod的一个子模块。因此,可以认为模块mod.sub会将其所有值都定义在table mod.sub中,也就是一个存储在table mod中且key为sub的table。一个“包(Package)”就是一个完整的模块树,它是Lua中发行的单位。
当require一个模块mod.sub时,require会用原始的模块名“mod.sub”作为key来查询table package.loaded和package.preload,其中,模块名中的点在搜索中没有任何 含义。
然而,当搜索一个定义子模块的文件时,require会将点转换为另一个字符,通常就是系统的目录分隔符。转换之后require就像搜索其他名称一样搜索这个名称。例如,假设路径为:
./?.lua;/usr/local/lua/?.lua;
并且目录分隔符为"/",那么调用require "a.b"就会尝试打开以下文件:
./a/b.lua
/usr/local/lua/a/b.lua
通过这样的加载策略,就可以将一个包中的所有模块组织到一个目录中。
Lua使用的目录分隔符是编译时配置的,可以是任意的字符串。
C函数名中不能包含点,因此一个用C编写的子模块a.b无法导出函数luaopen_a.b。所以,require会将点转换为下划线。例如,一个名为a.b的C程序库就应将其初始化函数命名为luaopen_a_b。在此又可以巧用连字符,来实现一些特殊的效果。例如,有一个C程序库a,先想将它作为mod的一个子模块,那么就可以将文件名改为mod/-a,当执行require "mod.-a"时,require就会找到改名后的文件mod/-a及其中的函数luaopen_a.
作为一项扩展功能,require在加载C子模块时还有一些选项。当require加载子模块时,无法找到对应的Lua文件或C程序库,它就会再次搜索C路径,不过这次将以包的名称来查找。例如,一个程序require子模块a.b.c,当无法找到文件a/b/c时,再次搜索就会找到文件a。如果找到了C程序库a,require就查看该程序库中是否有open函数luaopen_a_b_c。这项功能使得一个发行包可以将几个子模块组织到一个单一C程序库中,并且具有各自的open函数。
module函数也为子模块提供了显示的支持。当我们创建一个子模块时,调用module "a.b.c",module就会将环境table放入变量a.b.c,也就是“table a中的table b中的table c”。如果这些中间的table不存在,module就会创建它们。否则,就复用它们。
从Lua的观点看,同一个包中的子模块除了它们的环境table是嵌套的之外,它们之间并没有显示的关联性。require模块a并不会自动地加载它的任何子模块。同样,require子模块a.b也并不会自动地加载a。当然只要愿意,包的实现者完全可以实现这种关联。例如,模块a的一个子模块在加载时会显示地加载a。





















  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值