牛叔的lua研究报告——模块和包



niubo_
2010-07-09 13:12:56

[i=s] 本帖最后由 niubo_ 于 2010-7-9 13:14 编辑 [/i]

好吧,我承认这个副标题是抄袭PIL的,而且内容也几乎是现学现卖,不过这不过是个学习笔记性质的文章罢了,又不是自己出书……这事就这么定了。
前面说到简单编写一个模块有不少问题,下面我们来一点点解决。首先是以table名作为模块名不是个好主意,那么可以用一个局部变量M定义这个table,再把M赋值给一个代表模块的全局变量,模块下的函数也都以这个M作前缀,这样就消除了模块下函数对模块名的依赖关系。
不过一般说来模块名应该等同于文件名,在模块中的代码怎么知道自己的文件名呢?require在加载完一个模块后会以模块名作为唯一的参数来调用它,那么我们就可以在模块中用“...”获取参数:[code]
local modname = ...
[/code]但是模块名是个字符串,我们怎么把它绑到全局中去呢,这里用:[code]
_G[modname] = M
[/code]还有就是require需要一个返回值,我们要在最后返回这个模块。把所有跟定义模块相关的操作都放在最前面是个好主意。require调用完这个模块后如果没有获得返回值,返回package.loaded[modname]的值。那么我们就可以把模块挂在这里面:[code]
package.loaded[modname] = M
[/code]这样return语句也可以省掉了。不过还有一件事不爽,就是无论如何也要在定义公共函数的时候加个前缀,就算调用自己模块内的函数也要加前缀,局部函数不用加前缀,但是漏写local是很容易犯的错误,这就导致一个本该是私有的函数就跑到全局中的了,岂有此理……有个办法可以消除前缀,就是用到函数的环境。
从前有一口井,井底住着一只青蛙,青蛙每天抬起头只能看到井口大一片天空,于是青蛙一直以为天空也就只有这么大。我们的全局函数就是一只住在井底的青蛙,函数的环境就是青蛙头顶那一小片天空,函数看到的“全局”只是作为函数环境的table里那些东西而已。那么我们只需给模块一个单独的环境,然后所有在模块中定义的全局函数就都在这个作为环境的table里了,这时只需再把这个table以模块名放在package.loaded里。[code]
local modname = …
local M = {}
_G[modalname] = M
package.loaded[modname] = M
setfenv(1, M)
[/code]这样我们把公共函数定义成全局函数,私有函数定义成局部函数就可以了,就算忘了写local大不了就让这个函数变成本模块的公共函数而已,不会去污染全局空间。
可是现在新的问题又来了,我们现在把青蛙放在井里,啊,把函数放在环境里,但这个环境只是一个空的table而已,这个table就是它能看到的一切,也就是说现在我们在模块里连print都没法调用了。这也是上面的代码为什么把设置函数环境的语句放在最后的原因……
解决办法也有好几个,首先是用元表,在设定函数环境之前加上一句:[code]
setmetatable(M, {__index = _G})
[/code]然后还可以用一个局部变量引用_G,有点麻烦的地方就是要调用以前全局的东西前面统统要加上_G这个前缀了。位置也是在设定函数环境之前:[code]
local _G = _G
[/code]最后还有就是把要用的东西导成局部变量,这个比较类似Java“导包”的做法,可以导入一个变量或者整个模块:[code]
local print = print
local io = io
[/code]这三种方法速度是递增的,不过用起来貌似后两种比较麻烦些,这个看情况选择吧。

解决上面所说的这些问题后又导致了新的问题——每次开头都要写一段“解决问题”的代码,麻烦。于是终于轮到module函数出场了。简单说,一句module(...)就代替了下面这么一段代码:[code]
local modname = …
local M = {}
_G[modname] = M
package.loaded[modname] = M
setfenv(1, M)
[/code]前面说的解决访问外部变量的方法,对于后两种来说,在执行module(...)之前导出局部变量就行了,而对于第一种,module函数提供了可选参数,每一个作为参数的函数都会以模块为参数进行调用。这里有一个函数package.seeall(module),相当于setmetatable(modual, {__index = _G}),于是可以这么写:[code]
module(..., package.seeall)
[/code]下面再简单说说子模块和包。用require("a.b")的话,lua会把点替换成目录分隔符,这样就会去找a目录下的b.lua文件,于是b就成了a的子模块……按书中原意“包”的概念就是一个包含模块和其子模块整个树形结构的东西。因为lua加载模块时有一套文件搜索策略,加载模块a的话能加载a目录下的init.lua,所以a本身也是模块,不过按照Java中“包”的思想也可以把包理解为目录,目录下的lua文件为模块。当然更进一步说,可以把模块弄成类或者对象的形式,结构上和Java就更接近了……

写到这里我发现我已经无可救药的陷入学究派的泥潭了。lua本不是用来做大项目的语言,通过lua提供的机制可以实现,不过像上面看到的,解决问题的方法又带来了其他问题。对于模块的实现的确是偏于纯研究性质,实际用的话就算是直接用require把另一个模块中的所有东西都导入全局环境下其实也没什么……


w2jmoe
2010-07-09 13:22:35

原来这就是牛叔.....么.....牛....掌.....雷......还有多少啊...