Lua学习笔记 第十五章 模块与包

模块系统的一个主要目标是允许程序以不同的形式来共享代码。要实现共享就需要一些公共的规则。

Lua5.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来加载。注意,loadfileloadlib都只是加载了代码,并没有运行它们。

为了运行代码,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会将点转换为目录分隔符,之后就像搜索普通

文件名称一样来搜索这个名称。


你可能感兴趣的:(Lua)