技术说明 7

原文:https://www.lua.org/notes/ltn007.html

Technical Note 7 技术说明 7

Last update: Mon Aug 12 15:51:10 EST 2002

Modules & Packages 模块和包

by Roberto Ierusalimschy 罗伯托·伊鲁萨林斯基

Abstract 摘要

This note describes a simple way to implement modules (also called packages) in Lua. The proposed method provides namespaces, privacy, and some other benefits.

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

The Problem 问题

Many languages provide mechanisms to organize their space of global names, such as modules in Modula, packages in Java and Perl, and namespaces in C++. Each of these mechanisms has different rules regarding the use of elements declared inside a module, visibility rules, and other details. But all of them provide a basic mechanism to avoid collision among names defined in different libraries. Each library creates its own namespace, and names defined inside this namespace do not interfere with names in other namespaces.

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

Lua does not provide any explicit mechanism for packages. However, we can easily implement them using the basic mechanisms that the language provides. Actually, there are several ways to do that, and that creates a problem: There is no standard way to write a package in Lua. Moreover, it is up to you to follow the rules; there is neither a fixed way to implement packages, nor fixed operations to manipulate them.

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

Solutions 解决方案

A first solution, used by languages with no support for packages (such as C), is to choose a prefix, and use that prefix for all names in the package. (Lua itself is implemented that way; all its external names start with the prefix lua.) Despite its naivety, this is a quite satisfactory solution (at least it did not stop people from using C in huge projects).

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

In Lua, a better solution is to implement packages with tables: We only have to put our identifiers as keys in a table, instead of as global variables. The main point here is that we can store functions inside a table, just as any other value. For instance, suppose we are writing a library to manipulate complex numbers. We represent each number as a table, with fields r (real part) and i (imaginary part). To avoid polluting the global namespace, we will declare all our new operations in a table that acts as a new package:

​ 在 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

With this definition, we can use any complex operation qualifying the operation name, like this:

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

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

The use of tables for packages does not provide exactly the same functionality as provided by real packages. In Lua, we must explicitly put the package name in every function definition. Moreover, a function that calls another function inside the same package must qualify the name of the called function. We can ameliorate those problems using a fixed local name for the package (Public, for instance), and then assigning this local to the final name of the package. Following this guideline, we would write our previous definition like this:

​ 使用表格作为包并不能提供与真实包完全相同的功能。在 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
...

Whenever a function calls another function inside the same package (or whenever it calls itself recursively), it should access the called function through an upvalue of the local name of the package. For instance:

​ 无论何时函数在同一个包内调用另一个函数(或无论何时它递归地调用自身),它都应通过包的本地名称的上值访问被调用的函数。例如:

function Public.div (c1, c2)
  return %Public.mul(c1, %Public.inv(c2))
end

Following these guidelines, the connection between the two functions does not depend on the package name. Moreover, there is only one place in the whole package where we write the package name.

​ 遵循这些准则,两个函数之间的连接不依赖于包名称。此外,在整个包中只有一个地方我们写包名称。

Privacy 隐私

Usually, all names inside a package are exported; that is, they can be used by any client of the package. Sometimes, however, it is useful to have private names in a package, that is, names that only the package itself can use. A convenient way to do that is to define another local table for the private names in a package. That way, we distribute a package in two tables, one for public and the other for private names. Because we assign the public table to a global variable (the package name), all its components are accessible from the outside. But as we do not assign the private table to any global variable, it remains locked inside the package. To illustrate this technique, let us add to our example a private function that checks whether a value is a valid complex number. Our example now looks like this:

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

local Public, Private = {}, {}
Complex = Publicfunction Private.checkComplex (c)
  assert((type(c) == "table") and tonumber(c.r) and tonumber(c.i),
         "bad complex number")
endfunction Public.add (c1, c2)
  %Private.checkComplex(c1);
  %Private.checkComplex(c2);
  return {r=c1.r+c2.r, i=c1.i+c2.i}
end函数 Public.add (c1, c2) %Private.checkComplex(c1); %Private.checkComplex(c2); 返回 {r=c1.r+c2.r, i=c1.i+c2.i} 结束...

So, what are the pros and cons of this approach? All names in a package live in a separate namespace. Each entity in a package is clearly marked as public or private. Moreover, we have real privacy: Private entities are inaccessible outside the package. The main drawback of this approach is its verbosity when accessing other entities inside the same package: Every access needs a prefix (%Public. or %Private.). Despite the verbosity, these accesses are quite efficient; and we can mitigate this verbosity by providing shorter aliases for these two variables (with something like local E, I = Public, Private). There is also the problem that we have to change the prefixes whenever we change the status of a function between public and private. Nevertheless, I like this approach overall. For me, the negative side (its verbosity) is more than paid for by the simplicity of the language. After all, we can implement a quite satisfactory package system without needing any extra feature from the language.

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

Other Facilities 其他设施

An obvious benefit of using tables to implement packages is that we can manipulate packages like any other table, and use the whole power of Lua to create extra facilities. There are endless possibilities. Here we will give only a few suggestions.

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

We do not need to define all public items of a package together. For instance, we can add a new item to our Complex package in a separate chunk:

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

function Complex.div (c1, c2)
  return %Complex.mul(c1, %Complex.inv(c2))
end
function Complex.div (c1, c2)
return %Complex.mul(c1, %Complex.inv(c2))
end

(But notice that the private part is restricted to one file, which I think is a good thing.) Conversely, we can define more than one package in the same file. All we have to do is to enclose each one inside a do ... end block, so that its Public and Private variables are restricted to that block.

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

If we are going to use some operations often, we can give them global (or local) names:

​ 如果我们要经常使用某些操作,我们可以为它们提供全局(或局部)名称:

add = Complex.add
local i = Complex.ic1 = add(Complex.new(10, 20), i)

Or else, if we do not want to write the whole package name over and over, we can give a shorter local name to the whole package at once:

​ 或者,如果我们不想一遍又一遍地写整个包名,我们可以一次性给整个包一个较短的本地名称:

local C = Complex
c1 = C.add(C.new(10, 20), C.i)

It is easy to write a function that opens the whole package, putting all its names in the global namespace:

​ 编写一个打开整个包的函数很容易,它将所有名称都放在全局名称空间中:

function openpackage (ns)
  for n,v in ns do setglobal(n,v) end
end

If you are afraid of name clashes when opening a package, you can check the name before the assignment:

​ 如果您在打开包时担心名称冲突,可以在赋值之前检查名称:

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

Because packages themselves are tables, we can even nest packages; that is, we can create a whole package inside another one. However, such facility is seldom necessary.

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

Typically, when we write a package, we put its whole code in a single file. Then, to open or import a package (that is, to make it available) we just execute that file. For instance, if we have a file complex.lua with the definition of our complex package, the command dofile("complex.lua") will open the package. To avoid waste when a package is loaded multiple times, we can start a package checking whether it is already loaded:

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

if Complex then return endlocal Public, Private = {}, {}
Complex = Public...

Now, if you run dofile("complex.lua") when Complex is already defined, the whole file is skipped. (Notice: the new function require, to be available in Lua 4.1, will turn this check obsolete.)

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

最后修改 January 31, 2024: 更新 (e0896df)