Lua 学习之基础篇七<Lua Module,Package介绍>
Lua 之Module介绍
包管理库提供了从 Lua 中加载模块的基础库。 只有一个导出函数直接放在全局环境中: [require
]。 所有其它的部分都导出在表 package
中。
require (modname)
加载一个模块。 这个函数首先查找 [package.loaded
] 表, 检测 modname
是否被加载过。 如果被加载过,require
返回 package.loaded[modname]
中保存的值。 否则,它会为模块寻找加载器。
require
遵循 [package.searchers
] 序列的指引来查找加载器。如果改变这个序列,我们可以改变 require
如何查找一个模块。 下列说明基于 [package.searchers
]的默认配置。
首先 require
查找 package.preload[modname]
。 如果这里有一个值,这个值(必须是一个函数)就是那个加载器。 否则 require
使用 Lua 加载器去查找 [package.path
]的路径。 如果查找失败,接着使用 C 加载器去查找 [package.cpath
]的路径。 如果都失败了,再尝试一体化 加载器 。
每次找到一个加载器,require
都用两个参数调用加载器: modname
和一个在获取加载器过程中得到的参数。 (如果通过查找文件得到的加载器,这个额外参数是文件名。) 如果加载器返回非空值, require
将这个值赋给 package.loaded[modname]
。 如果加载器没能返回一个非空值用于赋给 package.loaded[modname]
, require
会在那里设入 true 。 无论是什么情况,require
都会返回 package.loaded[modname]
的最终值。
如果在加载或运行模块时有错误, 或是无法为模块找到加载器, require
都会抛出错误。
我们先看一下在lua文件中不显示require,lua运行环境会默认加载哪些, 可以通过遍历package.loaded数组来查看。
print("Before the require function , packages in the package.loaded :") for k ,v in pairs(package.loaded) do print(k,v) end
Before the require function , packages in the package.loaded : os table: 0x7ffc52403f00 table table: 0x7ffc524038e0 math table: 0x7ffc524054a0 package table: 0x7ffc524034a0 _G table: 0x7ffc524029b0 coroutine table: 0x7ffc52403fe0 bit32 table: 0x7ffc52403d60 utf8 table: 0x7ffc52405980 string table: 0x7ffc524051f0 debug table: 0x7ffc52404db0 io table: 0x7ffc52404490
如何通过require 来呼叫外部lua 文件
首先,创建一个moduleB.lua,内容如下
Jason={} function Jason.Sum(max) sum=0 for i=0,max,2 do --这个for循环用法是->i 以2的增长方式递增到max sum=sum+i end return sum end
其次,创建moduleA.lua
-- package.path = "/Users/jason/Desktop/reqtest/moduleB.lua" package.path = "./moduleB.lua" require"moduleB.lua" for k,v in pairs (package.loaded) do print (k,v) end print (package.loaded["moduleB.lua"]) print(Jason.Sum(100)) print(package.path) print(package.cpath)
输出为: debug table: 0x7fd9bec04db0 io table: 0x7fd9bec04490 string table: 0x7fd9bec051f0 moduleB.lua true math table: 0x7fd9bec054a0 bit32 table: 0x7fd9bec03d60 package table: 0x7fd9bec034a0 coroutine table: 0x7fd9bec03fe0 table table: 0x7fd9bec038e0 _G table: 0x7fd9bec029b0 utf8 table: 0x7fd9bec05980 os table: 0x7fd9bec03f00 true 2550 ./moduleB.lua /usr/local/lib/lua/5.3/?.so;/usr/local/lib/lua/5.3/loadall.so;./?.so
可以看到,在require相应的module后,package load会将其加载进来 并存储为true,我们可以利用这一点做文件load的check
dofile()
按参数filename
提供的文件名打开一个文件并将其内容作为一个Lua程序块执行,当省略参数fielname
时,函数默认把标准输入的内容作为程序块执行,执行结束后函数会把程序块返回的所有值作为函数的返回值返回,如果执行过程中发生了错误,函数会将错误向上跑出给它的调用者(当函数dofile()
不是运行在保护模式的状态下)。
用法是直接呼叫文件名,注意路径位置
dofile("./hellow.lua")
package
包是一种组织代码的方式。
使用表实现packages的明显的好处是:我们可以像其他表一样使用packages,并且可以使用语言提供的所有的功能,带来很多便利。大多数语言中,packages不是第一类值(first-class values)(也就是说,他们不能存储在变量里,不能作为函数参数。。。)因此,这些语言需要特殊的方法和技巧才能实现类似的功能。Lua中,虽然我们一直都用表来实现package,但也有其他不同的方法可以实现package.
例一
vector3d = {} -- 包名 function vector3d.function1() ...... end function vector3d.function2() ...... if (vector3d.function1()) then ...... end end return vector3d
这样定义的就是一个vector3d包,使用require语言打开这个包后,就可以使用 vector3d.function1和vector3d.function2这两个函数了。
这是最直接最好理解的一种Package定义方式,但是有一定的弊端。这个弊端主要体现在Package的实现过程中。可以看到,即使在
vector3d.function2()中使用function1()函数,也必须完整的加上vector3d包名,否则无法进行函数调用。
特别的注意最后的 return vector3d 语句,有了这句后调用者可以按照如下方式重命名包:
MyPackage = require "vector3d" MyPackage.function2()
例二:使用局部函数定义所有的Package内函数,然后在Package的结尾处将需要公开的函数直接放入Package中。代码像这样:
vector3d = {} -- 包名 local function function1() ...... end local function function2() ...... if (function1()) then ...... end end vector3d = {function1 = functoin1, function2function2 = function2 } return vector3d
最后给包中赋值的部分就是将需要的接口公开的部分。这样做的好处:不需要公开的函数可以完全隐藏起来(都是local函数);Package内部的各个函数相互之间调用的时候不再需要加Package名称进行区分; 可以按照需要随意的重命名Package公开的接口名称。
可以用local N = {}来保存数据和定义私有变量和函数。能明确的区分出接口和私有的定义,公开接口的名称还可以随意改变,这就意味着可以随意替换内部实现而不需要影响外部调用者。
package 相关函数介绍
package.config
一个描述有一些为包管理准备的编译期配置信息的串。 这个字符串由一系列行构成:
- 第一行是目录分割串。 对于 Windows 默认是 ‘
\
‘ ,对于其它系统是 ‘/
‘ 。 - 第二行是用于路径中的分割符。默认值是 ‘
;
‘ 。 - 第三行是用于标记模板替换点的字符串。 默认是 ‘
?
‘ 。 - 第四行是在 Windows 中将被替换成执行程序所在目录的路径的字符串。 默认是 ‘
!
‘ 。 - 第五行是一个记号,该记号之后的所有文本将在构建
luaopen_
函数名时被忽略掉。 默认是 ‘-
‘。
package.cpath
这个路径被 [
require
] 在 C 加载器中做搜索时用到。Lua 用和初始化 Lua 路径 [
package.path
]相同的方式初始化 C 路径 [package.cpath
] 。 它会使用环境变量LUA_CPATH_5_3
或 环境变量LUA_CPATH
初始化。 要么就采用luaconf.h
中定义的默认路径。package.loaded
用于 [
require
] 控制哪些模块已经被加载的表。 当你请求一个modname
模块,且package.loaded[modname]
不为假时, [require
]简单返回储存在内的值。这个变量仅仅是对真正那张表的引用; 改变这个值并不会改变 [
require
使用的表。package.loadlib(libname,funcname)
让宿主程序动态链接 C 库
libname
。当
funcname
为 "*
", 它仅仅连接该库,让库中的符号都导出给其它动态链接库使用。 否则,它查找库中的函数funcname
,以 C 函数的形式返回这个函数。 因此,funcname
必须遵循原型 [lua_CFunction
]这是一个低阶函数。 它完全绕过了包模块系统。 和 [
require
]不同, 它不会做任何路径查询,也不会自动加扩展名。libname
必须是一个 C 库需要的完整的文件名,如果有必要,需要提供路径和扩展名。funcname
必须是 C 库需要的准确名字 (这取决于使用的 C 编译器和链接器)。这个函数在标准 C 中不支持。 因此,它只在部分平台有效 ( Windows ,Linux ,Mac OS X, Solaris, BSD, 加上支持
dlfcn
标准的 Unix 系统)。package.path
这个路径被 [
require
] 在 Lua 加载器中做搜索时用到。在启动时,Lua 用环境变量
LUA_PATH_5_3
或环境变量LUA_PATH
来初始化这个变量。 或采用luaconf.h
中的默认路径。 环境变量中出现的所有 ";;
" 都会被替换成默认路径。package.preload
保存有一些特殊模块的加载器,这个变量仅仅是对真正那张表的引用; 改变这个值并不会改变 [
require
] 使用的表package.serachers
用于 [
require
]控制如何加载模块的表。这张表内的每一项都是一个 查找器函数。 当查找一个模块时, [
require
]按次序调用这些查找器, 并传入模块名([require
]的参数)作为唯一的一个参数。 此函数可以返回另一个函数(模块的 加载器)加上另一个将传递给这个加载器的参数。 或是返回一个描述为何没有找到这个模块的字符串 (或是返回 nil 什么也不想说)。Lua 用四个查找器函数初始化这张表。
第一个查找器就是简单的在 [
package.preload
]表中查找加载器。第二个查找器用于查找 Lua 库的加载库。 它使用储存在 [
package.path
] 中的路径来做查找工作。 查找过程和函数 [package.searchpath
描述的一致。第三个查找器用于查找 C 库的加载库。 它使用储存在 [
package.cpath
]中的路径来做查找工作。 同样, 查找过程和函数 [package.searchpath
]描述的一致。 例如,如果 C 路径是这样一个字符串"./?.so;./?.dll;/usr/local/?/init.so"
查找器查找模块
foo
会依次尝试打开文件./foo.so
,./foo.dll
, 以及/usr/local/foo/init.so
。 一旦它找到一个 C 库, 查找器首先使用动态链接机制连接该库。 然后尝试在该库中找到可以用作加载器的 C 函数。 这个 C 函数的名字是 "luaopen_
" 紧接模块名的字符串, 其中字符串中所有的下划线都会被替换成点。 此外,如果模块名中有横线, 横线后面的部分(包括横线)都被去掉。 例如,如果模块名为a.b.c-v2.1
, 函数名就是luaopen_a_b_c
。第四个搜索器是 一体化加载器。 它从 C 路径中查找指定模块的根名字。 例如,当请求
a.b.c
时, 它将查找a
这个 C 库。 如果找得到,它会在里面找子模块的加载函数。 在我们的例子中,就是找luaopen_a_b_c
。 利用这个机制,可以把若干 C 子模块打包进单个库。 每个子模块都可以有原本的加载函数名。除了第一个(预加载)搜索器外,每个搜索器都会返回 它找到的模块的文件名。 这和 [
package.searchpath
的返回值一样。 第一个搜索器没有返回值。package.searchpath (name, path [, sep [, rep]])
在指定
path
中搜索指定的name
。路径是一个包含有一系列以分号分割的 模板 构成的字符串。 对于每个模板,都会用
name
替换其中的每个问号(如果有的话)。 且将其中的sep
(默认是点)替换为rep
(默认是系统的目录分割符)。 然后尝试打开这个文件名。例如,如果路径是字符串
"./?.lua;./?.lc;/usr/local/?/init.lua"
搜索
foo.a
这个名字将 依次尝试打开文件./foo/a.lua
,./foo/a.lc
,以及/usr/local/foo/a/init.lua
。返回第一个可以用读模式打开(并马上关闭该文件)的文件的名字。 如果不存在这样的文件,返回 nil 加上错误消息。 (这条错误消息列出了所有尝试打开的文件名。)
定义Module的方式
定义module有两种方式,旧的方式,适用于Lua 5.0以及早期的5.1版本,新的方式现在均支持。
旧的方式:
通过module("...", package.seeall)来显示声明一个包。
--定义: -- oldmodule.lua module("oldmodule", package.seeall) function foo() print("oldmodule.foo called") end
--使用: require "oldmodule" oldmodule.foo()
1.module() 第一个参数就是模块名,如果不设置,缺省使用文件名。
- 2.第二个参数package.seeall,默认在定义了一个module()之后,前面定义的全局变量就都不可用了,包括print函数等,如果要让之前的全局变量可见,必须在定义module的时候加上参数package.seeall。 具体参考云风这篇文章
**package.seeall(module)功能:为module设置一个元表,此元表的__index字段的值为全局环境_G。所以module可以访问全局环境.**
之所以不再推荐module("...", package.seeall)这种方式,官方给出了两个原因。
1.package.seeall这种方式破坏了模块的高内聚,原本引入oldmodule只想调用它的foo()函数,但是它却可以读写全局属性,例如oldmodule.os.
2.第二个缺陷是module函数的side-effect引起的,它会污染全局环境变量。module("hello.world")会创建一个hello的table,并将这个table注入全局环境变量中,这样使得不想引用它的模块也能调用hello模块的方法。
新的方式: 通过return table来实现一个模块
--newmodule.lua local newmodule = {} function newmodule.foo() print("newmodule.foo called") end return newmodule
使用
local new = require "newmodule" new.foo()
因为没有了全局变量和module关键字,引用的时候必须把模块指定给一个变量。