Lua模块

Lua包库为Lua提供简易的加载和创建模块的方法,由require方法、module方法和package表组成...

从Lua5.1开始,对模块和包添加了新的支持,可使用modulerequire来定义和使用模块(module)和包(package)。在Lua中,模块是“第一类值”,一个模块就是一个程序库,可通过require(module)来加载后获得一个全局的table变量,这个table类似命名空间,其内容就是模块中导出的所有东西,如函数和变量。

module

模块定义的原始行为为

local module = ...
local M = {}
_G[module] = M
package.loaded[module] = M
-- setup for external access
setfenv(1, M)

Lua5.1提供新函数module(),调用时会创建表并将其赋予给全局变量和loaded table,最后还会将这个表设置为主程序块的环境。

默认情况下,module不提供外部访问,必须在调用前为需要访问的外部函数或模块声明适当的局部变量。也可以通过继承来实现外部访问,只需要在调用module时添加package.seeall选项。

module(..., package.seeall)
-- 等价于
setmetatable(M, {--index = _G}

模块的处理流程

# 建立一个模块
module(module_name, [, ...])

module(module_name, callback1, callback2, ...)
  1. 如果package.loaded[module_name]是一个table,则将这个table作为一个module
  2. 如果全局变量module_name是一个table,则将全局变量作为一个module
  3. 当前两种情况都不存在表module_name时,将新建一个table,并使其作为全局名为module_name的值,并执行package.loaded[module_name]...
  4. 依次调用回调函数
  5. 将当前模块的环境设置为module,同时将package.loaded[module_name] = module

模块的使用

-- 在模块文件中使用module函数
module "module_name"

--[[
等同语法
--]]

-- 定义模块名
local moduleName = "module_name"
-- 定义用于返回的模块表
local M = {}
-- 将模块表加入到全局变量
_G[moduleName] = M
-- 将模块表加入到package.loaded中防止多次加载
package.loaded[moduleName] = M
-- 将模块表设置为函数的环境表,使得模块中的所有操作是在模块表中,这样定义函数就直接定义在模块表中。
setfenv(1, M)

通过module()可以方便的编写模块中的内容,module指令运行完后,整个环境都被压栈,将导致前面全局的数据再也看不到了。

local _G = _G
module("module_name")

另一种巧妙的方式是Lua5.1提供了package.seeall作为moduleoption传入module("module_name", package.seeall)

通过module("module_name", package.seeall)来显式声明一个包,但官方不推荐使用这种方式,因为:

  • package.seeall的方式破坏了模块的高内聚,原本引入old_module指向调用它的foo()函数,但是它却可以读写全局属性,如old_module.os
  • module函数的side-effect,会污染全局环境变量。

所以,还是通过return table来实现模块更为优雅。

模块定义

有时候需要将一个模块重命名,以避免命名冲突。例如在测试中需加载同一模块的不同版本,而获得版本之间的性能区别。如何加载同一模块的不同版本呢?对于一个Lua文件而言,可以很轻易的重命名。但对于一个C程序库,是没有办法编辑其中的luaopen_*函数的名称的。

为了重命名的需求,require用到一个技巧:若一个模块名称中包含了连字符,require就会用连字符后的内容来创建luaopen_*函数名。如此一来,对于不同版本进行测试的需求即可迎刃而解了。

在Lua中创建一个模块最简单的方式是创建一个table,并将所有需要导出的函数放入其中,最后返回这个table

-- 定义全局变量的模块名称
modname = {} 

function modname.new(i,j)
  retun {i=i, j=j}
end

-- 定义常量
modname.i = modname.nex(0,1)

function modname.add(c1,c2)
  return modname.new(c1.i+c2.i, c1.j+c2j)
end

-- 返回模块的table
return module

缺陷:必须显式地将模块名放到每个函数定义中,函数调用时必须限定被调用函数名称。
思路:在模块中定义一个局部的table类型的变量,通过这个局部变量来定义和调用模块内的函数,然后将这个局部名称赋予模块的最终名称。

-- 定义局部变量
local M = {}
-- 将局部变量最终赋值给全局模块名
modname = M

function M.new(i,j)
  return {i=i, j=j}
end

-- 定义常量
M.i = M.new(0,1)

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

-- 返回模块的table
return modname

缺陷:模块内部其实使用的是一个局部变量,简单粗暴。模块内的函数仍需要一个前缀,如何完全避免写模块名称呢?
思路:消除前缀,将局部变量最终赋值给模块名。

$ vim mod.lua

-- 定义局部模块名称
local modname = ...

-- 打印参数
for i=1, select('#', ...) do
  print(select(i, ...))
end

-- 定义局部变量
local M = {} 
-- 将局部变量最终赋值给模块名
_G[modname] = M 
complex = M

function M.new(i,j)
  return {i=i, j=j}
end

-- 定义常量
M.i = M.new(0,1)

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

-- 返回模块的table
return complex

$ vim test.lua
require "mod"
c1 = mod.new(0,1)
c2 = mod.new(1,2)
ret  = mod.add(c1,c2)
print(ret.i, ret.j)

注意:

  • ...的作用是可以完全不用在模块中定义模块名称,若需重命名模块仅需重命名定义它的文件即可
  • return 定义模块时return是非常漏写的,是否可以将所有与模块相关的设置任务都集中在模块开头呢?

思路:消除return

  • 消除return方法是将模块table直接赋值给package.loaded即可
  • require会将模块名作为参数传递给模块,即无需return模块名称,因为若一个模块没有返回值的话,
  • require就会返回package.loaded[module]的当前值。
$ vim mod.lua

-- 三个点以避免模块重命名问题
local modname = ...
-- 局部变量
local M = {}
-- 将局部变量赋值给模块名
_G[modname] = M
-- 消除结尾return直接将模块赋值给package.loaded
package.loaded[modname] = M

package.loaded是什么呢?require会将返回值存储到package.loadedtable中,若加载器loader没有返回值,require会返回package.loadedtable的值。

缺陷:访问同一模块中其他函数时都需要添加限定名称,当模块内部的一个local函数由私有转换为公有后,相应的调用local函数的地方都需要修改。

思路:通过“函数环境”可解决这个问题,可以让模块的主程序块有一个独占的环境,这样不仅它的所有函数都可共享这个table,而且它的所有全局变量也都记录在这个table中,还可将所有函数声明为全局变量。这样他们就都自动地记录在一个独立的table中。而模块要做的就是将这个table赋予模块名和package.loaded

$ vim mod.lua

local modname =  ...

local M = {}
_G[modname ] = M

package.loaded[modname ] = M

-- 使用函数环境,无需return,因为模块无返回值,require会返回 package.loaded[modname]的当前值。
-- 当调用setfenv之后,将一个空table的M作为环境后,就无法访问前一个环境中全局变量了。
setfenv(1,M) -- 设置函数环境后就再也不能使用_G中table的内容了

function new(i,j)
  return {i=i, j=j}
end
function add(c1,c2)
  return new(c1.i+c2.i, c1.j+c2.j)
end 

缺陷:当调用setfenv之后,将一个空tableM作为环境后,就无法访问前一个环境中全局变量。
思路1:最简单的方式是使用元表,通过设置__index,模拟继承来实现。

$ vim mod.lua

local modname = ...

local M = {}
_G[modname] = M

package.loaded[modname] = M

setmetatable(M, {__index = _G})
setfenv(1, M)

缺陷:设置元表会有一点的开销
思路2:使用局部变量保存全局的环境变量,当访问前一个环境中的变量时,需添加前缀_G,由于没有涉及到元方法,此方式比第一种略快。

$ vim mod.lua

local modname = ...

local M = {}
_G[modname] = M

package.loaded[modname] = M

local _G = _G -- 保存全局的环境变量
setfenv(1, M)

思路3:最正规的方法是将那些需要用到的函数或模块声明为局部变量,此方式所需做的工作是最多的,但是性能是最好的。

$ vim mod.lua

local modname = ...

local M = {}
_G[modname] = M

package.loaded[modname] = M

-- 将所需使用的模块声明为局部变量先保存下来
local sqrt = math.sqrt
local io = io

setfenv(1,M)

综上所得,定义一个模块时的步骤如下:

  1. require传入的参数中获取模块名
local modname = ...
  1. 建立一个空的table
local M = {}
  1. 在全局环境_G中添加模块名对应的字段,将空table赋值给此字段。
_G[modname] = M
  1. 在已经加载的table中设置该模块
package.loaded[modname] = M
  1. 设置环境变量
setfenv(1, M)

为简化操作,Lua5.1+提供了module()函数,它包含了以上这些步骤完成的功能,在编写模块时,直接替代上述的操作。

module(...)

默认情况下,module不提供对外访问,也就是说你是无法访问前一个环境的,必须在调用它之前为所需访问的外部函数或模块声明强档的局部变量。也可以通过继承来实现外部访问。只需在module上添加package.seeall选项。

-- 功能相当于在之前基础上添加了 setmetatable(M, {__index=_G})
module(..., package.seeall)

require

Lua提供高级的require函数来加载运行库,简单来说requiredofile完成同样功能但有2点不同:

  1. require会搜索目录加载文件
  2. require会判断是否文件已经加载,避免重复加载同一个文件。

由于上述特征,require在Lua中是加载库的更好的函数。

加载指定的模块

# 加载指定的模块
require(module_name)

require()函数先检测package.loaded表中是否存在module_name,若存在则直接返回当中的值,若不存在则通过定义的加载器加载module_name

从Lua5.1+以后,Lua使用标准的模块管理库,所有模块加载都是通过require()完成。require()设计的颇具扩展性,它会从若干个已定义的loader中逐个尝试加载新的模块。系统库中提供4个loader,分别实现已加载模块、Lua模块、C扩展模块。这些loaderCFunction的形式存放在require的环境中的一个table中。若想更换Lua模块的加载方式,只需替换或增加一个新的loader即可。

local module = require('module_name')

require执行流程

  1. package.loaded中查找module_name
for k,v in pairs(package.loaded) do
    print(k, v)
end
print(math.pi);
  1. package.preload中查找module_name,若preload中存在则将其作为loader并调用loader(L)
  2. 根据package.path查找Lua文件

package.path保存加载外部模块的搜索路径,这种路径是“模板式的路径”,路径中会包含可替换符号?,这个符号会被替换然后Lua查找这个文件是否存在,若存在就会调用其中特定的接口。

package.path在虚拟机启动的是时候设置,若存在环境变量LUA_PATH则使用环境变量作为其值,并将环境变量中的;;替换为luaconf.h中定义的默认值,若不存在该变量就直接使用luaconf.h定义的默认值。

print(package.path)
  1. 根据packkage.cpath查找C库,并调用相应名称的接口。

package.cpath的作用和package.path一样,但它是用于加载第三方C库,其初始值可通过环境变量LUA_CPATH来设置。

package.loadlib(libname, func)相当于手工打开C库libname,并导出函数func后返回,loadlib其实是ll_loadlib

print(package.cpath)
C:\lua\?.dll;C:\lua\..\lib\lua\5.3\?.dll;C:\lua\loadall.dll;.\?.dll
function require(module)
    -- 判断模块是否已经被加载
    if not package.loaded[module] then
        -- 获取模块的加载器
        local loader = findloader(module)
        if loader==nil then
            error("unable to load module "..module)
        end
        -- 将模块标记为已加载
        package.loaded[module] = true
        -- 初始化模块
        local result = loader(module)
        if result~=nil then
            package.loaded[module] = result
        end
    end
    return package.loaded[module]
end

搜索目录加载文件

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

require使用的路径和普通路径是有些区别的,普通路径是一个目录列表,而require路径是一个模式列表,每个模式指明一种由虚文件名(require的参数)转成实文件名的方法。更加明确的说,每个模式都是一个包含可选的问号?的文件名。匹配时Lua会首先将问号?用虚文件名替换,然后查看文件是否存在。如若不存在则继续使用同样的方法用第二个模式匹配。

require关注的问题只有分号;(模式之间的分隔符)和问号?,其他的信息(目录分隔符,文件扩展名)在路径中定义。为了确定路径,Lua首先检查全局变量LUA_PATH是否为一个字符串,若是则认为此字符串就是路径。否则require会检查环境变量LUA_PATH的值。如果两者都是失败,require则使用固定的路径。

要加载一个模块,就必须知道模块在哪里。Windows平台中会根据环境变量Path来搜索,require使用的路径与传统路径不同,采用的路径是一连串的模式,其中每项都是将模块名转换为文件名的方式。requie会使用模块名来替换?。然后根据替换的结果来检查是否存在文件,若不存在则尝试下一项。路径中每项都是以分号分割。require只处理;?,其它的都由路径自己定义。

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

require的路径是一个模式列表,使用提供给require的虚文件名去替换模式中的问号,并判断文件是否存在,若不存在则使用第二个模式尝试匹配。为了让require能找到自己编写的Lua模块,需要把该模块的路径加入到LUA_PATH中,在LuaStudio中是package.path

Lua中有一个table用来保存所有加载过的文件列表,在LuaStudio中是package.loaded。可通过查看package.loaded表中是否存在所要加载的文件名来判断是否已经加载过。

实际编程中,require用于搜索的Lua文件的路径存放在变量package.path中。当Lua启动时,便以环境变量LUA_PATH的值来初始化变量package.path。若无LUA_PATH则使用一个编译时定义的默认路径来初始化。

$ lua
Lua 5.3.4  Copyright (C) 1994-2017 Lua.org, PUC-Rio

> print(package.path)
C:\lua\lua\?.lua;C:\lua\lua\?\init.lua;C:\lua\?.lua;C:\lua\?\init.lua;C:\lua\..\share\lua\5.3\?.lua;C:\lua\..\share\lua\5.3\?\init.lua;.\?.lua;.\?\init.lua

> print(package.cpath)
C:\lua\?.dll;C:\lua\..\lib\lua\5.3\?.dll;C:\lua\loadall.dll;.\?.dll

require 函数是如何加载模块的呢?

  • 首先检查package.loaded是否已经加载。
  • require为指定模块找到了一个Lua文件,它就会通过loadfile来加载该文件。
  • require无法找到与模式名相符的Lua文件,就会寻找C程序库,其搜索地址为package.cpath对应的路径。
  • 若找到的是一个C程序库,则通过loadlib来加载。

注意的是loadfileloadlib仅仅只是加载代码,并未运行他们。为了运行代码,require会以模块名作为参数来调用代码。

require & dofile & loadfile

Lua提供require()函数用来加载运行库,require()dofile()完成相同的功能,不同点是

  • require会搜索目录以加载文件
  • require不会重复加载同一模块
  • 若要让每次加载文件都执行,可使用dofile
  • 若加载后不执行,等需要时执行,可使用loadfile
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容