Lua 学习之基础篇七<Lua Module,Package介绍>

Dawnworld 2019-12-23

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 相关函数介绍

  1. package.config

    一个描述有一些为包管理准备的编译期配置信息的串。 这个字符串由一系列行构成:

  • 第一行是目录分割串。 对于 Windows 默认是 ‘\‘ ,对于其它系统是 ‘/‘ 。
  • 第二行是用于路径中的分割符。默认值是 ‘;‘ 。
  • 第三行是用于标记模板替换点的字符串。 默认是 ‘?‘ 。
  • 第四行是在 Windows 中将被替换成执行程序所在目录的路径的字符串。 默认是 ‘!‘ 。
  • 第五行是一个记号,该记号之后的所有文本将在构建 luaopen_ 函数名时被忽略掉。 默认是 ‘-‘。
  1. package.cpath

    这个路径被 [require] 在 C 加载器中做搜索时用到。

    Lua 用和初始化 Lua 路径 [package.path]相同的方式初始化 C 路径 [package.cpath] 。 它会使用环境变量 LUA_CPATH_5_3 或 环境变量 LUA_CPATH 初始化。 要么就采用 luaconf.h 中定义的默认路径。

  2. package.loaded

    用于 [require] 控制哪些模块已经被加载的表。 当你请求一个 modname 模块,且 package.loaded[modname] 不为假时, [require]简单返回储存在内的值。

    这个变量仅仅是对真正那张表的引用; 改变这个值并不会改变 [require 使用的表。

  3. 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 系统)。

  4. package.path

    这个路径被 [require] 在 Lua 加载器中做搜索时用到。

    在启动时,Lua 用环境变量 LUA_PATH_5_3 或环境变量 LUA_PATH 来初始化这个变量。 或采用 luaconf.h 中的默认路径。 环境变量中出现的所有 ";;" 都会被替换成默认路径。

  5. package.preload

    保存有一些特殊模块的加载器,这个变量仅仅是对真正那张表的引用; 改变这个值并不会改变 [require] 使用的表

  6. 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的返回值一样。 第一个搜索器没有返回值。

  7. 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关键字,引用的时候必须把模块指定给一个变量

相关推荐