第 14 章 Packages
很多语言专门提供了某种机制组织全局变量的命名,比如 Modula 的 modules,Java 和 Perl 的 packages,C++的 namespaces。每一种机制对在 package 中声明的元素的可见性以及其他一些细节的使用都有不同的规则。但是他们都提供了一种避免不同库中命名冲突的问题的机制。每一个程序库创建自己的命名空间,在这个命名空间中定义的名字
和其他命名空间中定义的名字互不干涉。
Lua并没有提供明确的机制来实现 packages。然而,我们通过语言提供的基本的机制很容易实现他。主要的思想是:像标准库一样,使用表来描述 package。
使用表实现 packages 的明显的好处是:我们可以像其他表一样使用 packages,并且可以使用语言提供的所有的功能,带来很多便利。大多数语言中,packages 不是第一类值(first-class values)(也就是说,他们不能存储在变量里,不能作为函数参数。。。)因此,这些语言需要特殊的方法和技巧才能实现类似的功能。
Lua中,虽然我们一直都用表来实现 pachages,但也有其他不同的方法可以实现 package,在这一章,我们将介绍这些方法。
14.1基本方法
第一包的简单的方法是对包内的每一个对象前都加包名作为前缀。例如,假定我们 正在写一个操作复数的库:我们使用表来表示复数,表有两个域r(实数部分)和 i(虚 数部分)。我们在另一张表中声明我们所有的操作来实现一个包:
complex = {} function complex.new (r, i) return {r=r, i=i} end -- defines aconstant `i' complex.i =complex.new(0, 1) function complex.add (c1, c2) return complex.new(c1.r + c2.r,c1.i + c2.i) end function complex.sub (c1, c2) return complex.new(c1.r - c2.r,c1.i - c2.i) end function complex.mul (c1, c2) return complex.new(c1.r*c2.r - c1.i*c2.i, c1.r*c2.i +c1.i*c2.r) end function complex.inv (c) local n = c.r^2+ c.i^2 return complex.new(c.r/n, -c.i/n) end return complex
这个库定义了一个全局名:coplex。其他的定义都是放在这个表内。 有了上面的定义,我们就可以使用符合规范的任何复数操作了,如:
c = complex.add(complex.i, complex.new(10, 20))
这种使用表来实现的包和真正的包的功能并不完全相同。首先,我们对每一个函数 定义都必须显示的在前面加上包的名称。第二,同一包内的函数相互调用必须在被调用函数前指定包名。我们可以使用国定的局部变量名,来改善这个问题,然后,将这个局 部变量赋值给最终的包。依据这个原则,我们重写上面的代码:
local P = {} complex = P --package name P.i ={r=0, i=1} function P.new (r, i) return {r=r, i=i} end function P.add (c1, c2) return P.new(c1.r + c2.r,c1.i + c2.i) end ...
当在同一个包内的一个函数调用另一个函数的时候(或者她调用自身),他仍然需要加上前缀名。至少,它不再依赖于国定的包名。另外,只有一个地方需要包名。可能你 注意到包中最后一个语句:
return complex
这个 return 语句并非必需的,因为 package 己经赋值给全局变量 complex 了。但是, 我们认为package 打开的时候返回本身是一个很好的习惯。额外的返回语句并不会花费什么代价,并且提供了另一种操作 package 的可选方式。
14.2私有成员(Privacy)
有时候,一个 package 公开他的所有内容,也就是说,任何 package 的客户端都可以访问他。然而,一个 package 拥有自己的私有部分(也就是只有 package 本身才能访 问)也是很有用的。在 Lua 中一个传统的方法是将私有部分定义为局部变量来实现。例 如,我们修改上面的例子增加私有函数来检查一个值是否为有效的复数:
local P = {} complex = P local function checkComplex (c) if not ((type(c) == "table") and tonumber(c.r) and tonumber(c.i)) then error("bad complex number", 3) end end function P.add (c1, c2) checkComplex(c1); checkComplex(c2); return P.new(c1.r + c2.r,c1.i + c2.i) end ... return P
这种方式各有什么优点和缺点呢?优点:package 中所有的名字都在一个独立的命名空间 中。Package 中的每一个实体(entity)都清楚地标记为公有还是私有。另外,我们实现 一个真正的隐私(privacy):私有实体在 package 外部是不可访问的。缺点:是访问同一个 package内的其他公有的实体写法冗余,必须加上前缀P.。还有一个大的问题是,当我们修改函数的状态(公有变成私有或者私有变成公有)我们必须修改函数得调用方式。
有一个有趣的方法可以立刻解决这两个问题。我们可以将 package 内的所有函数都 声明为局部的,最后将他们放在最终的表中。按照这种方法,上面的 complex package修改如下:
local function checkComplex (c) if not ((type(c) == "table") and tonumber(c.r) and tonumber(c.i)) then error("bad complex number", 3) end end local function new (r, i) return {r=r, i=i} end local functionadd (c1, c2) checkComplex(c1); checkComplex(c2); return new(c1.r + c2.r,c1.i + c2.i) end ... complex = { new =new, add = add, sub= sub, mul =mul, div = div, }
现在我们不再需要调用函数的时候在前面加上前缀,公有的和私有的函数调用方法
相同。在 package 的结尾处,有一个简单的列表列出所有公有的函数。可能大多数人觉得这个列表放在 package 的开始处更自然,但我们不能这样做,因为我们必须首先定义 局部函数。
14.3 包与文件
我们经常写一个 package 然后将所有的代码放到一个单独的文件中。然后我们只需 要执行这个文件即加载 package。例如,如果我们将上面我们的复数的 package代码放到 一个文件 complex.lua 中,命令"require complex"将打开这个 package。记住 require 命 令不会将相同的 package 加载多次。
需要注意的问题是,搞清楚保存 package 的文件名和 package 名的关系。当然,将 他们联系起来是一个好的想法,因为 require 命令使用文件而不是 packages。一种解决方 法是在 package 的后面加上后缀(比如.lua)来命名文件。Lua并不需要国定的扩展名, 而是由你的路径设置决定。例如,如果你的路径包含:"/usr/local/lualibs/?.lua",那么复 数 package可能保存在一个 complex.lua文件中。
有些人喜欢先命名文件后命名 package。也就是说,如果你重命名文件,package 也 会被重命名。这个解决方法提供了很大的灵活性。例如,如果你有两个有相同名称的 package ,你 不需要 修改 任何一 个, 只需要 重命 名一下 文件 。在 Lua 中 我 们使用
_REQUIREDNAME变量来重命名。记住,当 require 加载一个文件的时候,它定义了一个变量来表示虚拟的文件名。因此,在你的 package 中可以这样写:
local P = {} -- package if _REQUIREDNAME == nil then complex = P else _G[_REQUIREDNAME] = P end
代码中的 if 测试使 得 我们可 以不 需要 require 就可 以使 用 package 。如果_REQUIREDNAME 没有定义,我们用国定的名字表示 packageC例子中 complex)。另外,
package 使用虚拟文件名注册他自己。如果以使用者将库放到文件 cpx.lua 中并且运行 require cpx,那么 package 将本身加载到表 cpx 中。如果其他的使用者将库改名为 cpx_v1.lua 并且运行 require
cpx_v1,那么 package 将自动将本身加载到表 cpx_v1 当中。
14.4 使用全局表
上面这些创建 package 的方法的缺点是:他们要求程序员注意很多东西,比如,在 声明的时候也很容易忘掉 local 关键字。全局变量表的 Metamethods 提供了一些有趣的技 术,也可以用来实现 package。这些技术中共同之处在于:package 使用独占的环境。这很容易实现:如果我们改变了 package 主 chunk 的环境,那么由 package 创建的所有函数 都共享这个新的环境。
最简单的技术实现。一旦 package 有一个独占的环境,不仅所有她的函数共享环境, 而且它的所有全局变量也共享这个环境。所以,我们可以将所有的公有函数声明为全局变量,然后他们会自动作为独立的表(表指 package 的名字)存在,所有 package 必须 要做的是将这个表注册为 package 的名字。下面这段代码阐述了复数库使用这种技术的结果:
local P = {} complex = P setfenv(1, P)
现在,当我们声明函数 add,她会自动变成 complex.add:
function add (c1, c2) return new(c1.r + c2.r,c1.i + c2.i) end
另外,我们可以在这个 package中不需要前缀调用其他的函数。例如,add函数调用new函数,环境会自动转换为complex.new。这种方法提供了对 package 很好的支持:程 序员几乎不需要做什么额外的工作,调用同一个 package 内的函数不需要前缀,调用公 有和私有函数也没什么区别。如果程序员忘记了 local 关键字,也不会污染全局命名空间,只不过使得私有函数变成公有函数而己。另外,我们可以将这种技术和前一节我们使用
的 package 名的方法组合起来:
local P = {} -- package if _REQUIREDNAME == nil then complex = P else _G[_REQUIREDNAME] = P end setfenv(1, P)
这样就不能访问其他的 packages 了。一旦我们将一个空表 P作为我们的环境,我们就失去了访问所有以前的全局变量。下面有好几种方法可以解决这个问题,但都各有利 弊。最简单的解决方法是使用继承,像前面我们看到的一样:
local P = {} -- package setmetatable(P, { index =_G}) setfenv(1, P)
(你必须在调用 setfenv 之前调用 setmetatable,你能说出原因么?)使用这种结构, package 就可以直接访问所有的全局标示符,但必须为每一个访问付出一小点代价。理论上来讲,这种解决方法带来一个有趣的结果:你的 package 现在包含了所有的全局变 量。例如,使用你的 package 人也可以调用标准库的 sin 函数:complex.math.sin(x)。(Perl‘s package 系统也有这种特性)
另外一种快速的访问其他 packages 的方法是声明一个局部变量来保存老的环境:
<pre name="code" class="csharp">local P = {} pack= P local _G = _G setfenv(1, P)
现在,你必须对外部的访问加上前缀_G.,但是访问速度更快,因为这不涉及到 metamethod。与继承不同的是这种方法,使得你可以访问老的环境;这种方法的好与坏是有争议的,但是有时候你可能需要这种灵活性。一个更加正规的方法是:只把你需要的函数或者 packages 声明为 local:
local P = {} pack= P -- Import Section: -- declare everything this package needsfrom outside local sqrt = math.sqrt local io = io -- no moreexternal access afterthis point setfenv(1, P)<span style="font-size:14px; font-family: Arial, Helvetica, sans-serif; background-color: rgb(255, 255, 255);"> </span>
这一技术要求稍多,但他使你的 package 的独立性比较好。他的速度也比前面那几 种方法快。
14.5 其他-些技巧(Other Facilities)
正如前面我所说的,用表来实现 packages过程中可以使用 Lua 的所有强大的功能。 这里面有无限的可能性。建议:我们不需要将 package 的所有公有成员的定义放在一起,例如,我们可以在一个独 立分开的 chunk 中给我们的复数 package 增加一个新的函数:
function complex.div (c1, c2) return complex.mul(c1,complex.inv(c2)) end
(但是注意:私有成员必须限制在一个文件之内,我认为这是一件好事)反过来, 我们可以在同一个文件之内定义多个 packages,我们需要做的只是将每一个 package 放 在一个 do 代码块内,这样 local 变量才能被限制在那个代码块中。
在 package 外部,如果我们需要经常使用某个函数,我们可以给他们定义一个局部 变量名:
local add, i =complex.add, complex.i c1 =add(complex.new(10, 20), i)
如果我们不想一遍又一遍的重写 package 名,我们用一个短的局部变量表示 package:
local C = complex c1 =C.add(C.new(10, 20), C.i)
写一个函数拆开 package 也是很容易的,将 package 中所有的名字放到全局命名空 间即可:
function openpackage (ns) for n,v in pairs(ns) do _G[n] = v end end openpackage(complex) c1 =mul(new(10, 20), i)
如果你担心打开 package 的时候会有命名冲突,可以在赋值以前检查一下名字是否 存在:
function openpackage (ns) for n,v in pairs(ns) do if _G[n] ~= nil then error("name clash:" .. n .. " is alreadydefined") end _G[n] = v end end
由于 packages 本身也是表,我们甚至可以在packages 中嵌套 packages;也就是说我 们在一个package 内还可以创建 package,然后很少有必要这么做。
另一个有趣之处是自动加载:函数只有被实际使用的时候才会自动加载。当我们加 载一个自动加载的 package,会自动创建一个新的空表来表示 package 并且设置表的 index metamethod 来完成自动加载。当我们调用任何一个没有被加载的函数的时候,
indexmetamethod 将被触发去加载着个函数。当调用发现函数己经被加载, index 将 不会被触发。
下面有一个简单的实现自动加载的方法。每一个函数定义在一个辅助文件中。(也可 能一个文件内有多个函数)这些文件中的每一个都以标准的方式定义函数,例如:
function pack1.foo () ... end function pack1.goo () ... end
然而,文件并不会创建 package,因为当函数被加载的时候 package己经存在了。 在主 package 中我们定义一个辅助表来记录函数存放的位置:
local location = { foo = "/usr/local/lua/lib/pack1_1.lua", goo = "/usr/local/lua/lib/pack1_1.lua", foo1 = "/usr/local/lua/lib/pack1_2.lua", goo1 = "/usr/local/lua/lib/pack1_3.lua", }
下面我们创建 package 并且定义她的 metamethod:
</pre></div><pre name="code" class="csharp">pack1 = {} setmetatable(pack1, { index =function (t, funcname) local file = location[funcname] if not file then error("package pack1 doesnot define " .. funcname) end assert(loadfile(file))() -- loadand run definition return t[funcname] -- returnthe function end}) return pack1
加载这个 package 之后,第一次程序执行pack1.foo()将触发 index metamethod,接 着发现函数有一个相应的文件,并加载这个文件。微妙之处在于:加载了文件,同时返 回函数作为访问的结果。
因为整个系统都使用 Lua 写的,所以很容易改变系 统的行为。例如,函数可以是用 C 写的,在 metamethod 中用 loadlib 加载他。或者我们 我们可以在全局表中设定一个 metamethod 来自动加载整个packages.这里有无限的可能 等着你去发掘。