Lua中的模块(module)和包(package)详解

前言

从Lua5.1版本开始,就对模块和包添加了新的支持,可是使用require和module来定义和使用模块和包。require用于使用模块,module用于创建模块。简单的说,一个模块就是一个程序库,可以通过require来加载。然后便得到了一个全局变量,表示一个table。这个table就像是一个命名空间,其内容就是模块中导出的所有东西,比如函数和常量,一个符合规范的模块还应使require返回这个table。现在就来具体的总结一下require和module这两个函数。

require函数

Lua提供了一个名为require的函数用来加载模块。要加载一个模块,只需要简单地调用require “<模块名>”就可以了。这个调用会返回一个由模块函数组成的table,并且还会定义一个包含该table的全局变量。但是,这些行为都是由模块完成的,而非require。所以,有些模块会选择返回其它值,或者具有其它的效果。那么require到底是如何加载模块的呢?

首先,要加载一个模块,就必须的知道这个模块在哪里。知道了这个模块在哪里以后,才能进行正确的加载。当我们写下require “mod”这样的代码以后,Lua是如何找这个mod的呢?这里面就有说道了,我这里就详细的说一说。

在搜索一个文件时,在windows上,很多都是根据windows的环境变量path来搜索,而require所使用的路径与传统的路径不同,require采用的路径是一连串的模式,其中每项都是一种将模块名转换为文件名的方式。require会用模块名来替换每个“?”,然后根据替换的结果来检查是否存在这样一个文件,如果不存在,就会尝试下一项。路径中的每一项都是以分号隔开,比如路径为以下字符串:

代码如下:

?;?.lua;c:\windows\?;/usr/local/lua/?/?.lua

那么,当我们require “mod”时,就会尝试着打开以下文件:

代码如下:

mod
mod.lua
c:\windows\mod
/usr/local/lua/mod/mod.lua

可以看到,require函数只处理了分号和问好,其它的都是由路径自己定义的。在实际编程中,require用于搜索的Lua文件的路径存放在变量package.path中,在我的电脑上,print(package.path)会输出以下内容:

代码如下:

;.\?.lua;D:\Lua\5.1\lua\?.lua;D:\Lua\5.1\lua\?\init.lua;D:\Lua\5.1\?.lua;D:\Lua\5.1\?\init.lua;D:\Lua\5.1\lua\?.luac

如果require无法找到与模块名相符的Lua文件,那Lua就会开始找C程序库;这个的搜索地址为package.cpath对应的地址,在我的电脑上,print(package.cpath)会输出以下值:

代码如下:

.\?.dll;.\?51.dll;D:\Lua\5.1\?.dll;D:\Lua\5.1\?51.dll;D:\Lua\5.1\clibs\?.dll;D:\Lua\5.1\clibs\?51.dll;D:\Lua\5.1\loadall.dll;D:\Lua\5.1\clibs\loadall.dll

当找到了这个文件以后,如果这个文件是一个Lua文件,它就通过loadfile来加载该文件;如果找到的是一个C程序库,就通过loadlib来加载。loadfile和loadlib都只是加载了代码,并没有运行它们,为了运行代码,require会以模块名作为参数来调用这些代码。如果lua文件和C程序库都找不到,怎么办?我们试一下,随便require一个东西,比如:

代码如下:

require "jellythink"
lua: test.lua:1: module 'jellythink' not found:
     no field package.preload['jellythink']
     no file '.\jellythink.lua'
     no file 'D:\Lua\5.1\lua\jellythink.lua'
     no file 'D:\Lua\5.1\lua\jellythink\init.lua'
     no file 'D:\Lua\5.1\jellythink.lua'
     no file 'D:\Lua\5.1\jellythink\init.lua'
     no file 'D:\Lua\5.1\lua\jellythink.luac'
     no file '.\jellythink.dll'
     no file '.\jellythink51.dll'
     no file 'D:\Lua\5.1\jellythink.dll'
     no file 'D:\Lua\5.1\jellythink51.dll'
     no file 'D:\Lua\5.1\clibs\jellythink.dll'
     no file 'D:\Lua\5.1\clibs\jellythink51.dll'
     no file 'D:\Lua\5.1\loadall.dll'
     no file 'D:\Lua\5.1\clibs\loadall.dll'

是的,会报错的。以上就是require的一般工作流程。

奇淫技巧

可以看到,上面总结的都是通过模块的名称来使用它们。但有的时候需要将一个模块改名,以避免名称冲突。比如有这样的场景,在测试中需要加载同一模块的不同版本,而获得版本之间的性能区别。那么我们如何加载同一模块的不同版本呢?对于一个Lua文件来说,我们可以很轻易的改掉它的名称,但是对于一个C程序库来说,我们是没有办法编辑其中的luaopen_*函数的名称的。为了这种重命名的需求,require用到了一个小的技巧:如果一个模块名中包含了连字符,require就会用连字符后的内容来创建luaopen_*函数名。比如:如果一个模块的名称为a-b,require就会认为它的open函数名为luaopen_b,并不是luaopen_a-b。现在好了,对于上面提出的不同版本进行测试的需求,就可以迎刃而解了。

写一个我们自己的模块

在Lua中创建一个模块最简单的方法是:创建一个table,并将所有需要导出的函数放入其中,最后返回这个table就可以了。相当于将导出的函数作为table的一个字段,在Lua中函数是第一类值,提供了天然的优势。来写一个我们自己的模块,代码如下:

代码如下:

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
 
return complex  -- 返回模块的table

上面就是一个最简单的模块。在编写代码的过程中,会发现必须显式地将模块名放到每个函数定义中;而且,一个函数在调用同一个模块中的另一个函数时,必须限定被调用函数的名称,然而我们可以稍作变通,在模块中定义一个局部的table类型的变量,通过这个局部的变量来定义和调用模块内的函数,然后将这个局部名称赋予模块的最终的名称,代码如下:

代码如下:

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
 
function M.sub(c1, c2)
    return M.new(c1.r - c2.r, c1.i - c2.i)
end
 
return complex  -- 返回模块的table

这样,我们在模块内部其实使用的是一个局部的变量。这样看起来比较简单粗暴,但是每个函数仍需要一个前缀。实际上,我们可以完全避免写模块名,因为require会将模块名作为参数传给模块。让我们来做个试验:

代码如下:

local moduleName = ...
 
-- 打印参数
for i = 1, select('#', ...) do
     print(select(i, ...))
end
 
local M = {}    -- 局部的变量
_G[moduleName] = 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
 
function M.sub(c1, c2)
    return M.new(c1.r - c2.r, c1.i - c2.i)
end
 
return complex  -- 返回模块的table

将上述代码保存为test1.lua。再写一个文件,代码如下:

代码如下:

require "test"
 
c1 = test.new(0, 1)
c2 = test.new(1, 2)
 
ret = test.add(c1, c2)
print(ret.r, ret.i)

将上述代码保存为test2.lua

将上述代码放在同一个文件夹下,运行test2.lua文件,打印结果如下:

代码如下:

test1
1     3

(PS:如果对代码中的三个点(…)不熟悉的同学,请参考:《Lua中的函数》一文)经过这样的修改,我们就可以完全不用在模块中定义模块名称,如果需要重命名一个模块,只需要重命名定义它的文件就可以了。

细心的同学可能注意到了模块结尾处的return语句,这样的一个return语句,在定义模块时,是非常容易漏写的,怎么办?如果将所有与模块相关的设置任务都集中在模块开头,就会更好了。消除return语句的一种方法是,将模块table直接赋值给package.loaded,代码如下:

代码如下:

local moduleName = ...
 
local M = {}    -- 局部的变量
_G[moduleName] = M     -- 将这个局部变量最终赋值给模块名
 
package.loaded[moduleName] = M
-- 后续代码省略

示例代码下载:点击这里下载

package.loaded是什么?

require会将返回值存储到table package.loaded中;如果加载器没有返回值,require就会返回table package.loaded中的值。可以看到,我们上面的代码中,模块没有返回值,而是直接将模块名赋值给table package.loaded了。这说明什么,package.loaded这个table中保存了已经加载的所有模块。现在我们就可以看看require到底是如何加载的呢?

1.先判断package.loaded这个table中有没有对应模块的信息;
2.如果有,就直接返回对应的模块,不再进行第二次加载;
3.如果没有,就加载,返回加载后的模块。

再说“环境”

大家可能注意到了,当我访问同一个模块中的其它函数时,都需要限定名称,就比如上面代码中的M。当我把模块内部的一个local函数由私有改变成公有以后,相应的调用local函数的地方都需要修改,加上限定名称。怎么办?总不能每次都修改代码吧。如何一次搞定?是否还记得《Lua中的环境概念》这篇博文,里面讲到的环境概念在这里就能派上用场。

我们可以让模块的主程序块有一个独占的环境,这样不仅它的所有函数都可共享这个table,而且它的所有全局变量也都记录在这个table中,还可以将所有公有函数声明为全局变量,这样它们就都自动地记录在一个独立的table中。而模块所要做的就是将这个table赋予模块名和package.loaded。比如以下代码就可以完成:

代码如下:

local moduleName = ...
 
local M = {}    -- 局部的变量
_G[moduleName] = M     -- 将这个局部变量最终赋值给模块名
 
package.loaded[moduleName] = M
setfenv(1, M)

这之后,当我们写下下面的代码:

代码如下:

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

它其实是和下面的代码是等价的:

代码如下:

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

当我调用同一个模块中的函数new时,也不用指定M了。这样就可以让我们在写自己的模块时,省去了前缀;还有其它好处,你可以自己想想。但是,当我们调用setfenv之后,将一个空table M作为环境后,就无法访问前一个环境中全局变量了。这该如何是好?现在提供几种方法。

方法一:

最简单的方法就是在《Lua中的环境概念》一文中说的那样,使用元表,设置__index,模拟继承来实现。代码如下:

代码如下:

local moduleName = ...
 
local M = {}    -- 局部的变量
_G[moduleName] = M     -- 将这个局部变量最终赋值给模块名
 
package.loaded[moduleName] = M
 
setmetatable(M, {__index = _G})
setfenv(1, M)

上述代码很简单,原理在之前的博文中都详细的讲过了,这里不再啰嗦了。由于需要设置元表,所有会有一定的开销,但是可以忽略的。

方法二:

代码如下:

local moduleName = ...
 
local M = {}    -- 局部的变量
_G[moduleName] = M     -- 将这个局部变量最终赋值给模块名
 
package.loaded[moduleName] = M
 
local _G = _G -- 保存了全局的环境变量
setfenv(1, M)

这样在自己的模块中保存一个全局的环境变量,当我们访问前一个环境中的变量时,就需要添加前缀_G,貌似有点小麻烦。但是,由于没有涉及到元方法,这种方法会比方法一略快。

方法三:

这种方法是最正规的方法,就是将那些需要用到的函数或模块声明为局部变量,看以下代码:

代码如下:

local moduleName = ...
 
local M = {}    -- 局部的变量
_G[moduleName] = M     -- 将这个局部变量最终赋值给模块名
 
package.loaded[moduleName] = M
 
local sqrt = math.sqrt -- 在我们自己的模块中需要用到math.sqrt这个函数,所以就先保存下来
local io = io -- 需要用到io库,也保存下来
setfenv(1, M) -- 设置完成以后,就不能再使用_G table中的内容了

方法三需要做的工作是最多的,而且也是最麻烦的,但是性能是最好的。怎么用,你自己看着办吧。

module函数

大家可能也注意到了,在定义一个模块时,前面的几句代码都是一样的,就分为以下几步:

1.从require传入的参数中获取模块名;
2.建立一个空table;
3.在全局环境_G中添加模块名对应的字段,将空table赋值给这个字段;
4.在已经加载table中设置该模块;
5.设置环境变量。

就是这几步,在每一个模块的定义之前都需要加上,是不是有点麻烦,在Lua5.1中提供了一个新函数module,它包括了以上这些步骤完成的功能。在编写一个模块时,可以直接用以下代码来取代前面的设置代码:

代码如下:

module(...)

就上面这一小句代码,它会创建一个新的table,并将其赋予给模块名对应的全局字段和loaded table,最后还会将这个table设为主程序块的环境。默认的情况下,module不提供外部的访问的,也就是说,你无法访问前一个环境了,在再说“环境”一节,我专门说了三种解决方案。在使用module时是这样解决的:

代码如下:

module(..., package.seeall)

这句话的功能就好比之前的功能再加上了setmetatable(M, {__index = _G})。有了这一句代码,基本上就可以说万事不愁了。

子模块与包

Lua支持具有层级性的模块名,可以用一个点来分隔名称中的层级。假设一个模块名为mod.sub,那么它就是mod的一个子模块。因此,可以认为模块mod.sub会将其所有值都定义在table mod.sub中,也就是一个存储在table mod中,且key为sub的table。就好比下述的定义:

代码如下:

local mod = {sub = {}}

当require一个模块mod.sub时,require会用原始的模块名“mod.sub”作为key来查询table package.loaded和package.preload,其中,模块名中的点在搜索时没有任何意义。但是,当搜索一个定义子模块的文件时,require会将点转换成另一个字符,通常就是系统的目录分隔符,转换之后require就像搜索其他名称一样来搜索这个名称。比如路径为以下字符串:

代码如下:

?;?.lua;c:\windows\?;/usr/local/lua/?/?.lua

那么,当我们require “mod.sub”时,就会尝试着打开以下文件:

代码如下:

mod\sub
mod\sub.lua
c:\windows\mod\sub
/usr/local/lua/mod/mod/sub.lua

通过这样的加载策略,就可以将一个包中的所有模块组织到一个目录中。像这些小的功能,都会组合成很多的奇淫技巧,虽然在实际项目中用的不会很多,但是玩起来还是很有意思的。

总结

这一篇文章主要总结了Lua中的两个非常重要的函数require和module。希望对大家有用。对于今天的开发来说,什么都讲究模块开发,而这篇文章总结的就是进行模块开发时需要使用的两个重要函数。大家在日后构建自己的模块时,如果有哪里不懂,哪里不清楚,可以再回过头来阅读这篇文章,或者可以直接留言和我交流。我相信,分享与交流使我们更进步。

(0)

相关推荐

  • Lua教程(十一):模块与包详解

    从Lua 5.1开始,我们可以使用require和module函数来获取和创建Lua中的模块.从使用者的角度来看,一个模块就是一个程序库,可以通过require来加载,之后便得到一个类型为table的全局变量.此时的table就像名字空间一样,可以访问其中的函数和常量,如: 复制代码 代码如下: require "mod" mod.foo() local m2 = require "mod2" local f = mod2.foo f() 1. require函数:

  • Lua调用自定义C模块

    这是<Lua程序设计>中提到的,但是想成功执行,对于初学Lua的确没那么简单.这里涉及如何如何生成一个动态链接库so文件:Lua5.2中导出函数从LuaL_register变成了LuaL_newlib.对于具体的细节有待深入.这里的模块名是hello_lib, Lua解释器会根据名字找到对应的模块,而后执行其中的 luaopen_XXX方法. 代码: #include <math.h> #include <lua5.2/lua.h> #include <lua5.

  • 解析Lua中的全局环境、包、模块组织结构

    模块就是一个程序库,而包是一系列模块.Lua中可以通过require来加载模块,然后得到一个全局变量表示一个table.Lua将其所有的全局变量保存在一个被称为"环境"的常规table中.本文首先介绍环境的一些实用技术,然后介绍如何引用模块及编写模块的基本方法. 1. 环境 Lua将环境table保存在一个全局变量_G中,可以对其访问和设置.有时我们想操作一个全局变量,而它的名称却存储在另一个变量中,或者需要通过运行时的计算才能得到,可以通过value = _G[varname]来获得

  • 使用Lua编写Nginx服务器的认证模块的方法

    过去两天里,我解决了一个非常有趣的问题.我用一个nginx服务器作为代理,需要能够向其中添加一个认证层,使其能够使用外部的认证源(比如某个web应用)来进行验证,如果用户在外部认证源有账号,就可以在代理里认证通过. 需求一览 我考虑了几种解决方案,罗列如下: 用一个简单的Python/Flask模块来做代理和验证. 一个使用subrequests做验证的nginx模块(nginx目前可以做到这一点) 使用Lua编写一个nginxren认证模块 很显然,给整个系统添加额外请求将执行的不是很好,因为

  • Lua中使用模块的一些基础知识

    --两个横线开始单行的注释,--[[加上两个[和]表示多行的注释--]]. 复制代码 代码如下: -- 假设文件mod.lua的内容是: local M = {} local function sayMyName()   print('Hrunkner') end function M.sayHello()   print('Why hello there')   sayMyName() end return M 复制代码 代码如下: -- 另一个文件也可以使用mod.lua的函数: local

  • 在Lua中使用模块的基础教程

     什么是模块? 模块是一个像,可以使用需要加载并有包含表中的单个全局命名的库.该模块可包含若干函数和变量.所有这些函数和变量被包裹在以它作为一个命名空间的表.也是一个很乖的模块有必要的规定,返回此表上所需要的. Lua模块 表中的模块的使用可以帮助我们以多种方式,使我们能够操纵模块中我们操纵任何其他lua的表相同的方式.作为操纵模块的能力的结果,它提供了额外的功能的量等语言需要特殊的机制.由于lua模块,这个免费的方式下,用户可以调用Lua函数以多种方式.如下面几个: 复制代码 代码如下: --

  • Lua模块与包学习笔记

    从 Lua 5.1 开始,Lua 加入了标准的模块管理机制,可以把一些公用的代码放在一个文件里,以API 接口的形式在其他地方调用,有利于代码的重用和降低代码耦合度. 创建模块 其实 Lua 的模块是由变量.函数等已知元素组成的 table,因此创建一个模块很简单,就是创建一个 table,然后把需要导出的常量.函数放入其中,最后返回这个 table 就行.格式如下: 复制代码 代码如下: -- 定义一个名为 module 的模块 module = {}   -- 定义一个常量 module.c

  • Lua极简入门指南(六):模块

    从用户的角度来看,一个模块能够通过 require 加载并返回一个 table,模块导出的接口都被定义在此 table 中(此 table 被作为一个 namespace).所有的标准库都是模块.标准库被预先加载了,就像这样: 复制代码 代码如下: math = require 'math' string = require 'string' require 函数 使用 require 函数加载模块能够避免多次重复加载模块.加载一个模块: 复制代码 代码如下: require 'modulena

  • Lua中的模块与module函数详解

    很快就要开始介绍Lua里的"面向对象"了,在此之前,我们先来了解一下Lua的模块. 1.编写一个简单的模块 Lua的模块是什么东西呢?通常我们可以理解为是一个table,这个table里有一些变量.一些函数- 等等,这不就是我们所熟悉的类吗? 没错,和类很像(实际上我说不出它们的区别).   我们来看看一个简单的模块,新建一个文件,命名为game.lua,代码如下: 复制代码 代码如下: game = {} function game.play()     print("那么

  • Lua模块和模块载入浅析

    在lua中,我们可以直接使用requeire("model_name")来载入别的文件,文件的后缀名是.lua,载入的时候直接执行那个文件了. 比如:my.lua 文件中 复制代码 代码如下: print("hello world!") 当我require("my")时,那么会直接输出hello world! 特别注意: 1.用require载入相同的文件时,只有第一次执行,以后都不执行. 2.如果你想让每次载入都执行文件,那么可以使用dofil

  • Lua的函数环境、包实例讲解

    复制代码 代码如下: function foo()    print(g or "No g defined!") end foo() setfenv(foo, { g = 100, print = print }) --设置foo的环境为表{ g=100, ...} foo() print(g or "No g defined!") --No g defined! --100 --No g defined! 定义:函数环境就是函数在执行时所见的全局变量的集合,以一个

随机推荐