Lua 技术说明 7

模块和程序包

作者:Roberto Ierusalimschy

摘要

本说明描述了在 Lua 中实现模块(也称为程序包)的一种简单方法。所提出的方法提供了命名空间、隐私和其他一些好处。

问题

许多语言都提供了组织其全局名称空间的机制,例如 Modula 中的模块、Java 和 Perl 中的程序包以及 C++ 中的命名空间。每种机制对于模块内声明的元素的使用、可见性规则和其他细节都有不同的规定。但它们都提供了一种基本机制来避免不同库中定义的名称之间的冲突。每个库都创建自己的命名空间,该命名空间内定义的名称不会干扰其他命名空间中的名称。

Lua 没有提供任何显式的程序包机制。但是,我们可以轻松地使用该语言提供的基本机制来实现它们。实际上,有几种方法可以做到这一点,这就产生了一个问题:没有一种标准方法来编写 Lua 程序包。此外,遵守规则取决于您自己;既没有固定的方法来实现程序包,也没有固定的操作来操作它们。

解决方案

第一个解决方案是由不支持程序包的语言(例如 C)使用的,即选择一个前缀,并将其用于程序包中的所有名称。(Lua 本身就是以这种方式实现的;其所有外部名称都以前缀lua开头。)尽管这种方法很幼稚,但它是一个非常令人满意的解决方案(至少它没有阻止人们在大型项目中使用 C)。

在 Lua 中,一个更好的解决方案是使用表来实现包:我们只需要将我们的标识符作为键放在表中,而不是作为全局变量。这里的主要目的是我们可以将函数存储在表中,就像任何其他值一样。例如,假设我们正在编写一个库来处理复数。我们将每个数字表示为一个表,其中包含字段 r(实部)和 i(虚部)。为了避免污染全局命名空间,我们将在充当新包的表中声明所有新操作

Complex = {}
Complex.i = {r=0, i=1}

function Complex.new (r, i) return {r=r, i=i} end

function Complex.add (c1, c2)
  return {r=c1.r+c2.r, i=c1.i+c2.i}
end

function Complex.sub (c1, c2)
  return {r=c1.r-c2.r, i=c1.i-c2.i}
end

function Complex.mul (c1, c2)
  return {r = c1.r*c2.r - c1.i*c2.i,
          i = c1.r*c2.i + c1.i*c2.r}
end

function Complex.inv (c)
  local n = c.r^2 + c.i^2
  return {r=c.r/n, i=c.i/n}
end

有了这个定义,我们可以使用任何复杂的操作来限定操作名称,如下所示

c = Complex.add(Complex.i, Complex.new(10, 20))

使用表作为包并不能提供与真实包完全相同的功能。在 Lua 中,我们必须在每个函数定义中明确放置包名称。此外,在同一包内调用另一个函数的函数必须限定被调用函数的名称。我们可以使用包的固定局部名称(例如 Public)来改善这些问题,然后将此局部名称分配给包的最终名称。按照此准则,我们将编写如下前一个定义

local Public = {}
Complex = Public           -- package name

Public.i = {r=0, i=1}
function Public.new (r, i) return {r=r, i=i} end

...
每当一个函数在同一包内调用另一个函数(或每当它递归地调用自身)时,它都应该通过包的局部名称的上值来访问被调用的函数。例如
function Public.div (c1, c2)
  return %Public.mul(c1, %Public.inv(c2))
end
遵循这些准则,两个函数之间的连接不依赖于包名称。此外,在整个包中只有一个地方我们编写包名称。

隐私

通常,包中的所有名称都是导出的;也就是说,它们可以被包的任何客户端使用。然而,有时在包中使用私有名称很有用,也就是说,只有包本身才能使用的名称。一种方便的方法是在包中为私有名称定义另一个局部表。这样,我们就可以在一个包中分发两个表,一个用于公共名称,另一个用于私有名称。因为我们将公共表分配给一个全局变量(包名),所以它的所有组件都可以从外部访问。但是,由于我们没有将私有表分配给任何全局变量,因此它仍然锁定在包内。为了说明此技术,让我们在示例中添加一个私有函数,用于检查值是否为有效的复数。我们的示例现在如下所示

local Public, Private = {}, {}
Complex = Public

function Private.checkComplex (c)
  assert((type(c) == "table") and tonumber(c.r) and tonumber(c.i),
         "bad complex number")
end

function Public.add (c1, c2)
  %Private.checkComplex(c1);
  %Private.checkComplex(c2);
  return {r=c1.r+c2.r, i=c1.i+c2.i}
end

...

那么,这种方法有什么优点和缺点呢?包中的所有名称都存在于一个单独的命名空间中。包中的每个实体都明确标记为公共或私有。此外,我们有真正的隐私:私有实体在包外不可访问。这种方法的主要缺点是它在访问同一包内的其他实体时很冗长:每次访问都需要一个前缀(%Public.%Private.)。尽管冗长,但这些访问非常有效;我们可以通过为这两个变量提供更短的别名(类似于 local E, I = Public, Private)来减轻这种冗长。还有一个问题是,每当我们在公共和私有之间更改函数的状态时,我们都必须更改前缀。尽管如此,我总体上喜欢这种方法。对我来说,负面(冗长)完全被语言的简单性所弥补。毕竟,我们可以在不需要语言的任何额外功能的情况下实现一个非常令人满意的包系统。

其他工具

使用表来实现包的一个明显好处是,我们可以像操作任何其他表一样操作包,并使用 Lua 的全部功能来创建额外的工具。可能性是无穷无尽的。在这里,我们只提供一些建议。

我们不需要同时定义包的所有公共项。例如,我们可以在一个单独的块中向我们的 Complex 包添加一个新项

function Complex.div (c1, c2)
  return %Complex.mul(c1, %Complex.inv(c2))
end
(但请注意,私有部分仅限于一个文件,我认为这是一件好事。)相反,我们可以在同一文件中定义多个包。我们所要做的就是将每个包都包含在一个 do ... end 块中,以便其 PublicPrivate 变量仅限于该块。

如果我们经常使用某些操作,我们可以给它们全局(或局部)名称

add = Complex.add
local i = Complex.i

c1 = add(Complex.new(10, 20), i)
或者,如果我们不想一遍又一遍地写整个包名,我们可以一次性给整个包一个较短的局部名称
local C = Complex
c1 = C.add(C.new(10, 20), C.i)

编写一个打开整个包的函数很容易,它会将所有名称放入全局名称空间

function openpackage (ns)
  for n,v in ns do setglobal(n,v) end
end
openpackage(Complex)
c1 = mul(new(10, 20), i)
如果您在打开包时担心名称冲突,可以在赋值之前检查名称
function openpackage (ns)
  for n,v in ns do
    if getglobal(n) ~= nil then
      error(format("name clash: `%s' is already defined", n))
    end
    setglobal(n,v)
  end
end

因为包本身是表,所以我们甚至可以嵌套包;也就是说,我们可以在另一个包中创建一个完整的包。但是,这种设施很少需要。

通常,当我们编写一个包时,我们会将它的全部代码放在一个文件中。然后,要打开或导入一个包(即,使其可用),我们只需执行该文件。例如,如果我们有一个文件 complex.lua,其中包含我们复杂包的定义,那么命令 dofile("complex.lua") 将打开该包。为了避免在多次加载包时浪费,我们可以启动一个包来检查它是否已经加载

if Complex then return end

local Public, Private = {}, {}
Complex = Public

...
现在,如果您在 Complex 已经定义的情况下运行 dofile("complex.lua"),则整个文件将被跳过。(注意:新函数 require,将在 Lua 4.1 中提供,将使此检查过时。)


最后更新:2002 年 8 月 12 日星期一下午 3:51:10 美国东部时间