Lua_第 14 章 Packages

第 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.这里有无限的可能 等着你去发掘。

时间: 2024-11-04 23:40:47

Lua_第 14 章 Packages的相关文章

《TCP/IP详解卷1:协议》第14章 DNS:域名系统---读书笔记

<TCP/IP详解卷1:协议>第14章 DNS:域名系统---读书笔记 1.引言 5.指针查询 DNS中一直难于理解的部分就是指针查询方式,即给定一个IP地址,返回与该地址对应的域名. 当一个组织加入Internet,并获得DNS域名空间的授权,如noao.edu,则它们也获得了对应IP地址的in-addr.arpa域名空间的授权.在noao.edu这个例子中,它的网络号是140.252的B类网络.在DNS树中结点in-addr.arpa的下一级必须是该IP地址的第一字节(例中为140),再下

JavaScript高级程序设计(第三版)学习笔记13、14章

第13章,事件 事件冒泡 IE的事件叫做事件冒泡:由具体到不具体 <!DOCTYPE html> <html> <head> <title>Event Bubbling Example</title> </head> <body> <div id="myDiv">Click Me</div> </body> </html> 如果你单击了<div>

Lua_第19章 String 库(上)

Lua_第19章String 库 Lua解释器对字符串的支持很有限.一个程序可以创建字符串并连接字符串,但不能截取子串,检查字符串的大小,检测字符串的内容.在 Lua中操纵字符串的功能基本来自于 string 库. String 库中的一些函数是非常简单的:string.len(s)返回字符串 s 的长度;string.rep(s, n)返回重复 n 次字符串 s 的串;你使用 string.rep("a", 2^20)可以创建一个 1M bytes 的字符 串(比如,为了测试需要);

Lua_第19章 String 库(下)

Lua_第19章 String 库(下) 19.3捕获(Captures) Capture(下面译为捕获或者capture,模式中捕获的概念指,使用临时变量来保存匹配的子模式,常用于 向前引用.)是这样一种机制:可以使用模式串的一部分匹配目标串的一部分.将你想捕 获的模式用圆括号括起来,就指定了一个capture.在 string.find 使用captures 的时候,函数会返回捕获的值作为额外的结果.这常被用 来将一个目标串拆分成多个: pair = "name =Anna" _,

&lt;&lt;精通iOS开发&gt;&gt;第14章例子代码小缺陷的修复

大熊猫猪·侯佩原创或翻译作品.欢迎转载,转载请注明出处. 如果觉得写的不好请多提意见,如果觉得不错请多多支持点赞.谢谢! hopy ;) 首先推荐大家看这本书,整本书逻辑非常清晰,代码如何从无到有,到丰满说的很有条理. 说实话本书到目前为止错误还是极少的,不过人无完人,在第14章前半部分项目的代码中,作者在MasterVC到DetailVC中又直接添加了一个segue,该segue的ID为"masterToDetail",作用是当新建一个tinyPix文档时可以直接跳转到DetailV

第 14 章 迭代器模式【Iterator Pattern】

以下内容出自:<<24种设计模式介绍与6大设计原则>> 周五下午,我正在看技术网站,第六感官发觉有人在身后,扭头一看,我C,老大站在背后,赶忙站起来, “王经理,你找我?” 我说. “哦,在看技术呀.有个事情找你谈一下,你到我办公室来一下.” 老大说. 到老大办公室, “是这样,刚刚我在看季报,我们每个项目的支出费用都很高,项目情况复杂,人员情况也不简单,我看着 也有点糊涂,你看,这是我们现在还在开发或者维护的103 个项目,你能不能先把这些项目信息重新打印一份 给我,咱们好查查到

C++ primer plus读书笔记——第14章 C++中的代码重用

第14章 C++中的代码重用 1. 使用公有继承时,类可以继承接口,可能还有实现(基类的纯虚函数提供接口,但不提供实现).获得接口是is-a关系的组成部分.而使用组合,类可以获得实现,但不能获得接口.不继承接口是has-a关系的组成部分. 2. C++还有另一种实现has-a关系的途径——私有继承.使用私有继承,基类的公有成员和保护成员都将称为派生类的私有成员.这意味着基类方法将不会称为派生类对象公有接口的一部分,但可以在派生类的成员函数中使用它们. 3. 包含将对象作为一个命名的成员对象添加到

14章类型信息之使用类字面常量

14章类型信息-之类型转换前先做检查--之使用类字面常量--类名.class--以及动态instanceof(isInstance方法)----递归计数(计算各个类的个数) 实例代码: 实体类父类: //: typeinfo/pets/Individual.javapackage typeinfo.pets; public class Individual implements Comparable<Individual> {  private static long counter = 0;

第14章 multimap多重映照容器

/* 第14章 multimap多重映照容器 14.1 multimap技术原理 14.2 multimap应用基础 14.3 本章小结 */ // 第14章 multimap多重映照容器 // 14.1 multimap技术原理 ------------------------------------------------------------------------------ // 14.2 multimap应用基础 ----------------------------------