跳转至内容

Pascal 编程/单元

来自维基教科书,开放的书籍,开放的世界

在原始的标准 Pascal 中,除了 Pascal 本身定义的标准函数之外,程序的所有功能都必须在一个文件中定义,即 program 源代码文件。虽然在教学的背景下,源代码仍然比较短,但整个应用程序很快就变得杂乱无章,尽管使用各种注释来结构化文本。

很快,就出现了各种将程序模块化的尝试。最值得注意的实现,至今仍在使用,是 UCSD Pascal 的单元概念。

UCSD Pascal 单元

[编辑 | 编辑源代码]

一个 UCSD Pascal 单元就像一个 program,除了它不能独立运行,而是应该被 program 使用。一个 unit 可以像任何 program 一样定义常量、类型、变量和例程,但它没有可以独立运行的可执行部分。使用一个单元意味着该单元成为程序的一部分;这类似于将单元的整个源代码复制到 program 中,但并不完全相同。

通常,单元存储在单独的文件中,从而极大地清理了 program 的源代码文件。然而,这不是一个硬性要求,因为在模块的 end. 之后,该模块被认为是完整的,可以跟随另一个模块。

Note 从现在开始,作为另一层抽象,模块是指 programunit。(在 FP 中,library 也是另一种模块类型。)

定义单元

[编辑 | 编辑源代码]

一个 unit 定义与普通的 program 非常相似,但具有许多额外的功能。

一个 unit 的第一行看起来像这样

unit myGreatUnit;

program 不同,它没有参数列表。一个 unit 是一个包含特定功能的自包含单元,因此无法以任何方式进行参数化。[fn 1]

这一行还声明了一个新的标识符,在这个例子中是 myGreatUnitMyGreatUnit 成为所谓完全限定标识符的第一部分。稍后将详细介绍。

unit 概念提供了封装其定义的方法,因此使用该 unit 的程序员不需要了解特定功能的实现方式。

这是通过将 unit 分成两部分来实现的

  1. interface 部分,以及
  2. implementation 部分。

使用另一个单元的程序员只需要知道如何使用该单元:这在 interface 部分中概述。另一方面,编程该单元的程序员需要在 implementation 部分中实现该单元的功能。因此,一个最基本的单元看起来像这样

unit myGreatUnit;
interface
implementation
end.

interface 部分必须 implementation 部分之前。还要注意, units 使用 end. 终止,就像 program 一样。

interface 部分包含一个块,但它不能包含任何语句。 interface 仅仅是声明性的。在 interface 部分中定义的所有标识符都将成为“公共的”,也就是说,使用该单元的程序员可以访问它们。另一方面,在 implementation 部分中定义的所有标识符都是“私有的”:它们只能在单元自己的 implementation 部分使用。没有办法绕过这种导出和“私有”代码的分离。

unit randomness;

// public  - - - - - - - - - - - - - - - - - - - - - - - - -
interface

// a list of procedure/function signatures makes
// them usable from outside of the unit
function getRandomNumber(): integer;

// a definition (an implementation) of a routine
// must not be in the interface-part

// private - - - - - - - - - - - - - - - - - - - - - - - - -
implementation

function getRandomNumber(): integer;
begin
	// chosen by fair dice roll
	// guaranteed to be random
	getRandomNumber := 4;
end;

end.

使用单元

[编辑 | 编辑源代码]

现在,我们终于将一些代码外包出去,这是件好事,但这一切的目的是要使用外包的代码。为此,UCSD Pascal 定义了 uses 子句。一个 uses 子句指示编译器导入另一个单元的代码,并熟悉该单元 interface 部分中声明的所有标识符。因此,来自单元 interface 部分的所有标识符都将可用,就好像它们是通过 uses 子句导入它们的模块的一部分一样。以下是一个示例

program chooseNextCandidate(input, output);
uses
	// imports a unit
	randomness;

begin
	writeLn('next candidate: no. ', getRandomNumber());
end.

注意,program chooseNextCandidate 既没有定义也没有声明函数 getRandomNumber,但仍然使用了它。由于 getRandomNumber 的签名列在 interface 部分的 randomness 中,因此它可用于其他使用该模块的模块。

Note 每个 program 最多只能有一个 uses 子句。它必须出现在程序头之后。

Uses 子句在任何模块中都是允许的。当然,可以在 unit 内使用其他单元。此外,您可以在一个 unit 中使用两个 uses 子句,一个在 interface 中,另一个在 implementation 部分中。列在 interface 部分 uses 子句中的单元会传播,这意味着它们也会传播到使用这些单元的模块中。[fn 2][fn 3]

命名空间

[edit | edit source]

现在,如果所有编写的单元都必须显式定义独占标识符,那么使用单元编程将会很麻烦。但情况并非如此。随着模块的出现,所有模块都隐式地构成了一个命名空间。命名空间是一个自包含的范围,其中只有范围内的标识符需要是唯一的。您可以随意定义自己的 getRandomNumber 并仍然使用 randomness 单元。

为了区分来自不同命名空间的标识符,可以通过在标识符之前添加命名空间名称来限定标识符,两者之间用点隔开。因此,randomness.getRandomName 清楚地标识了由 randomness 单元导出的 getRandomNumber 函数。这种表示法称为完全限定标识符,简称为 FQI。

优先级

[edit | edit source]

依赖项

[edit | edit source]

更多功能

[edit | edit source]

初始化和最终化部分

[edit | edit source]

不带源代码的发布

[edit | edit source]

单元设计

[edit | edit source]

需要考虑一些因素

  • 无论何时,如果某些代码可能对其他程序也有用,您可能希望创建一个单独的单元。
  • 一个单元应该提供所有必要的才能使其有用的功能,但是,
  • 一个单元不应该提供与其主要目的无关的功能。
  • 单元的可用性很大程度上取决于定义良好的接口。需要了解具体的实现通常是代码质量差的指标。

特殊单元

[edit | edit source]

运行时系统

[edit | edit source]

一些编译器使用单元来提供某些功能,这些功能服务于编译器实际任务和 program (即您编写的) 之间的灰色区域。最值得注意的是,Delphi、FPC 以及 GPC 提供了一个运行时系统 (RTS),其中包含作为语言的一部分定义的所有标准例程(例如 writeLnord)。在 Delphi 和 FPC 中,该单元称为 system,而 GPC 则带有一个 GPC 单元。这些单元有时被称为运行时库,简称为 RTL。

Note 由于这些单元提供了 Pascal 的标准例程,因此它们必须作为第一个单元导入。但是,由于人们容易犯错,编译器将负责确保 RTS 首先加载。因此,禁止编写 uses system;。如果您尝试手动导入 RTLFPC 会发出错误。

了解 RTS’s 单元叫什么名字可能很有用,因为这意味着 RTL 的所有标识符都是一个命名空间的一部分。这意味着,在(例如)Delphi 和 FP 中,可以使用其短名称以及 FQI system.abs 来引用标准函数 abs。如果您在当前范围内遮蔽了 abs 函数,但需要使用 Pascal 自身的 abs 函数,则可能需要后者。

调试

[edit | edit source]

FPC 带有一个名为 heapTrc(堆跟踪)的特殊单元。此单元提供了一个内存管理器。它用于找出 program 是否未释放之前为其保留的任何内存块。分配内存而不将其返还给 OS 被称为“内存泄漏”,这是一种非常糟糕的情况。由于 heapTrc 单元对 Pascal 的内存管理具有侵入性行为,因此它也需要在 system 单元加载后立即加载。因此,FPC 禁止您在 uses 子句中显式包含 heapTrc 单元,但提供了 -gh 编译器命令行开关,它将确保包含该单元。

heapTrc 单元仅在开发阶段使用。它可以在 program 的最后一个 end. 后打印内存报告。

heapTrc 单位使用起来比较简单,但也功能有限。我们建议使用专用的调试和性能分析工具,例如 valgrind(1),因为了解如何使用此类工具将对您在将来切换编程语言时有很大帮助。如果您在调用 fpc(1) 时指定了 -gv 开关,FPC 将插入调试信息,以便与 valgrind(1) 一起使用。

其他模块化实现

[编辑 | 编辑源代码]

扩展 Pascal 标准为 module 定义了规范。这些提供了更高级的模块化方法。但是,FPC 和 Delphi 都不支持它,只有 GPC 支持。

编写一个在程序终止时说再见的单位,即在终端打印一条消息。
您可以利用 finalization 部分作为钩子来实现该行为。
unit friendly;
interface
implementation
finalization
begin
	writeLn('Goodbye!');
end;
end.
您可以利用 finalization 部分作为钩子来实现该行为。
unit friendly;
interface
implementation
finalization
begin
	writeLn('Goodbye!');
end;
end.

备注

  1. 扩展 Pascal 标准中描述的 module 概念确实允许模块参数化。
  2. 截至 3.0.4 版本,FPC 不支持标识符的传播。
  3. PXSC 样式的模块需要使用 use global someModuleName 来启用标识符的传播。


下一页: 面向对象 | 上一页: 作用域
首页: Pascal 编程
华夏公益教科书