Lua学习笔记--模块与包

学习完了函数,那么,一堆函数就成了一个模块,一堆模块就是一个包。今天来学习一下怎么写一个模块和怎么调用模块。


一.简介

Lua的感觉就是简洁,自由,一个万能的table可以搞定所有的事情。Lua从5.1开始提供了require(用于加载模块)和module(用于创建模块)的两个函数增加对模块的支持,当然,不使用这两个关键字也是可以用table自己实现模块加载的。
模块就是一个程序库,可以通过require加载,然后我们就得到了一个全局变量,是一个包含这个模块所有内容的table。其实感觉require就像#include一样,将我们需要的内容包含进来。先看一个简单的例子:
--加载另一个模块
require "mod"
--调用另一个模块中的func函数
mod.func()
当然,还可以把模块设置成一个较短的名称,供局部使用,或者设置模块中的某个函数为较短的名称:
--设置模块的局部名称
local m = require "mod"
--设置模块函数局部名称
local f = m.func
--使用
m.func()
--或者
f()

二.关于全局变量

我们之前用的变量,如果没有加local关键字,那就是全局变量。Lua将所有的全局变量都保存在了一个常规的table中,这个table称为“环境”,这个table被命名为_G。这种实现方式一方面不需要再额外为全局变量创建数据结构,另一方面,我们也可以像操作普通table那样操作全局的table。
一个例子,看一下_G中都有神马鬼,lua代码如下:
--定义一个全局变量
a = 100;
--自己定义全局函数
function testfunc()
	print("hehe")
end
--输出Lua中的全局变量
for n in pairs(_G) do
	print(n)
end
结果:
ipairs
load
package
require
coroutine
select
pairs
rawlen
tonumber
assert
type
os
io
math
debug
next
setmetatable
error
rawset
tostring
dofile
_VERSION
getmetatable
print
table
rawequal
string
rawget
_G
testfunc
pcall
utf8
xpcall
loadfile
a
collectgarbage
请按任意键继续. . .

我们自己定义了两个全局变量和函数,不写local就是全局的,然后打印所有的全局变量,发现这两个也被打出来了。其他的就是偶们熟悉的比如tostring等等方法。但是,这样的话,如果我们的程序比较大,那么就会特别凌乱。大家都是全局的,像找一个函数也比较费劲,还可能有命名冲突,这时候,模块就该登场了。


三.编写一个简单的模块

最简单的方式,就是建立一个全局的table,然后编写一堆函数,添加到这个table中去,然后在模块的最后把这个table返回,例如:
--定义一个模块

--模块名
func = {}

--模块内部的内容加上模块名
function func.test1()
	print("test1 func!")
end

function func.test2()
	print("test2 func!")
end

func.num = 10

--最后需要将模块名返回
return func 
这样,一个最简单的模块就编写好了,我们使用的时候,只需要加载这个模块就可以了。但是,这个模块的设计有很多很多缺陷,不过作为测试暂时够了,先看看怎么使用这个模块。

四.使用一个模块

加载一个模块还是相对比较简单的,通过require就可以搞定一切。就是require "模块名",其实require就是负责一个加载的过程,关于table等都是模块内部写好的,并且返回给require。

关于require的详细操作:

function require (name)
    if not package.loaded[name] then	--判断模块是否已经加载了,避免重复加载
        local loader = findload(name)
        if loader == nil then
            error("unable to 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
分析一下上面的操作:
1.当我们给出模块名后,Lua并不是无脑的去加载,而是先去package.loaded这个table中查看,如果这个表中已经有了这个模块,那么就不会重复加载这个模块。然后通过loader加载相关模块。如果我们想要强制重新加载一个模块,那么需要先将其删除,即package.loaded["mod"]=nil,然后再重新require即可。
2.关于findload,因为Lua支持Lua本身加载模块,也支持加载C程序库的模块,加载Lua模块使用loadfile,加载C程序库使用loadlib。注意,这一步lua并不会运行这些模块,仅仅是加载。
3.如果开始加载了,就将package.load[mod]置为true,防止有关联加载时,返回又加载本模块,导致死循环。
4.加载完之后,会将package.loaded[mod]置为加载的模块名,注意这个地方,如果加载器中没有返回模块名,即我们写模块的时候没有返回名字,那么就返回package.loaded中的值。

使用一下上面编写好的模块:

模块文件func.lua
--定义一个模块

--模块名
func = {}

--模块内部的内容加上模块名
function func.test1()
	print("test1 func!")
end

function func.test2()
	print("test2 func!")
end

func.num = 10

--最后需要将模块名返回
return func 
测试文件test.lua:
local m = require "func"

--使用模块中的内容
m.test1()
m.test2()
print(m.num)

print("\n")

--再看一下全局变量的情况
for n in pairs(_G) do
	print(n)
end
结果:
test1 func!
test2 func!
10

setmetatable
rawget
next
tostring
getmetatable
load
rawset
pairs
pcall
os
coroutine
xpcall
dofile
print
select
type
table
tonumber
func
error
debug
utf8
math
ipairs
loadfile
rawlen
string
collectgarbage
_VERSION
rawequal
io
package
require
_G
assert
请按任意键继续. . .

可见,我们使用了另一个模块中的内容,实现了功能的模块化。而且,这次的全局变量中只有func一个,即我们这个模块,不会再造成特别麻烦的命名冲突等的问题。

关于模块路径的问题:

还有一个很关键的问题,我们写好了一个模块,要放在哪里才能被Lua加载呢?按照国际惯例,放在当前路径是肯定可以的,刚才的例子就是将test.lua,func.lua都放在了编译出来的Lua解释器的路径下的。
搜索一个文件时,在windows上,很多都是根据windows的环境变量path来搜索,而require所使用的路径与传统的路径不同,require采用的路径是一连串的模式,其中每项都是一种将模块名转换为文件名的方式。require会用模块名来替换每个“?”,然后根据替换的结果来检查是否存在这样一个文件,如果不存在,就会尝试下一项。路径中的每一项都是以分号隔开,比如路径为以下字符串:
?;?.lua;c:\windows\?;/usr/local/lua/?/?.lua
那么,lua就会尝试加载下面的内容:
mod
mod.lua
c:\windows\mod
/usr/local/lua/mod/mod.lua

那么,这样,我们就知道Lua加载的默认规则了,但是,万一我们想把Lua的东东全都放在一个目录里面,想改一下这个默认的设置,要怎么办呢?
上面的那一串内容存储在package的一个字段中,而且,Lua对于Lua模块和C模块是分开的,Lua的路径在packet.path中,C的在packet.cpath中。我们打印一下瞧瞧:
--输出Lua默认的加载搜寻路径
print("Lua:", package.path)
--输出C默认的加载搜寻路径
print("C:",package.cpath)

结果:
Lua:    C:\Users\puppet_master\Desktop\LuaTest\Lua Scrip\lua\?.lua;C:\Users\puppet_master \Desktop\LuaTest\Lua Scrip\lua\?\init.lua;C:\Users\zhangjian \Deskto
p\LuaTest\Lua Scrip\?.lua;C:\Users\puppet_master\Desktop\LuaTest\Lua Scrip\?\ini
t.lua;C:\Users\puppet_master\Desktop\LuaTest\Lua Scrip\..\share\lua\5.3\?.lua;C:
\Users\puppet_masterDesktop\LuaTest\Lua Scrip\..\share\lua\5.3\?\init.lua;.\?.l
ua;.\?\init.lua
C:      C:\Users\puppet_master\Desktop\LuaTest\Lua Scrip\?.dll;C:\Users\puppet_master\Desktop\LuaTest\Lua Scrip\..\lib\lua\5.3\?.dll;C:\Users\puppet_master\Desk
top\LuaTest\Lua Scrip\loadall.dll;.\?.dll
请按任意键继续. . .

好大一串,那么,我们如果想改的话,要在哪里改呢?当然好改了,我们有Lua的源码啊,想改什么改什么。在luaconf.h中的LUA_PATH中定义了这个路径:
#define LUA_PATH_DEFAULT  \
		LUA_LDIR"?.lua;"  LUA_LDIR"?\\init.lua;" \
		LUA_CDIR"?.lua;"  LUA_CDIR"?\\init.lua;" \
		LUA_SHRDIR"?.lua;" LUA_SHRDIR"?\\init.lua;" \
		".\\?.lua;" ".\\?\\init.lua"
#define LUA_CPATH_DEFAULT \
		LUA_CDIR"?.dll;" \
		LUA_CDIR"..\\lib\\lua\\" LUA_VDIR "\\?.dll;" \
		LUA_CDIR"loadall.dll;" ".\\?.dll"
如果想要进行修改就可以改啦。


关于模块名字和模块文件名的问题:
这个我感觉还是一样的好。省去不少麻烦,还好记。

五.编写一个真正的模块

上面编写了一个测试是模块,但是这个模块漏洞百出,非常不严格。下面,逐步修改一下这个模块:

为了方便修改模块名称:

上面的那个模块,直接定义了一个全局的变量,为了防止冲突,这个全局变量必定比较长,模块中每个东东都要加上这么长的一个前缀显然不合适,更要名的是,入过我想改掉模块的名字,那么,所有的东东的名字就都得重新写,这简直是灾难。所以,一种偷懒的方法油然而生:
--定义一个模块

--本地模块名
local M = {}
--真正的模块名
func = M


--模块内部的内容加上模块名
function M.test1()
	print("test1 func!")
end

function M.test2()
	print("test2 func!")
end

M.num = 10

--最后需要将模块名返回
return func 
这种方法就是用了一个本地的,简单的变量,操作本地的内容,将内容都创建好了,把这个table的引用再给真正的全局引用,然后返回真正的模块名即可。

如果模块名和模块所在的文件不同名时,会发生什么?

我也不知道,事实胜于雄辩:

func.lua文件:
--定义一个模块

--本地模块名
local M = {}
--真正的模块名
mod = M


--模块内部的内容加上模块名
function M.test1()
	print("test1 func!")
end

function M.test2()
	print("test2 func!")
end

M.num = 10

--最后需要将模块名返回
return mod 
test.lua文件:
--require 必须跟模块文件名,直接给模块名找不到的
--这里的文件名不考虑后缀的问题,lua会自动补全后缀
require "func"
for n in pairs(_G) do
	print(n)
end
结果:
xpcall
collectgarbage
utf8
require
next
select
package
rawset
error
ipairs
tostring
getmetatable
rawequal
type
dofile
table
os
print
coroutine
string
setmetatable
load
pairs
rawlen
mod
_G
debug
loadfile
pcall
io
assert
tonumber
math
_VERSION
rawget
请按任意键继续. . .

例子中,模块名为mod,模块文件名为func,require时跟的是文件名,但是加载出来之后仍然是模块原来的名字。所以,这样搞乱七八糟,一般而言,一个文件作为一个模块再好不过,所以文件名和模块名还是一致为好,否则加载的名字和使用的名字不一样,蛋疼得要命。


通过改文件名改模块名:

既然一个模块就是一个文件了,就更想偷懒了,当我们想改名字的时候,改文件名就可以改掉模块名,那该多好啊。下面是一个例子:
--定义一个模块

--由于require函数会将需要的模块名传递给该函数,所以...就代表了模块名
local modname = ...
--本地模块名
local M = {}
--真正的模块名直接祖册到全局变量中
_G[modname] = M


--模块内部的内容加上模块名
function M.test1()
	print("test1 func!")
end

function M.test2()
	print("test2 func!")
end

M.num = 10

这样,require这个模块的时候,模块名会传递给...,这个就作为模块名,就是文件名。所以,我们就不需要自己定义文件名了。








你可能感兴趣的:(Lua)