Delphi 11 编程语言的完整介绍 作者:Marco Cantu 译者:豆豆爸
1.4 程序的结构
你几乎不可能在一个文件中编写所有代码,不过本章前面展示的第一个简单控制台应用程序就是这种情况。只要创建了可视化应用程序,就会在项目文件旁边得到至少一个辅助源代码文件。
这个附带文件被称为单元(unite),用 PAS 扩展名表示(代表 Pascal 源文件单元),而主项目文件则使用 DPR 扩展名(代表 Delphi PRoject 文件)。这两个文件都包含 Object Pascal 源代码。
Object Pascal 广泛使用单元或程序模块。事实上,即使不使用对象,单元也可以用来提供模块化和封装能力。单元就像命名空间。一个 Object Pascal 应用程序通常由多个单元组成,包括托管窗体和数据模块的单元。事实上,当你在项目中添加一个新窗体时,集成开发环境实际上就添加了一个新单元,单元里定义了新窗体的代码。
单元不需要定义窗体;它们可以简单地定义并提供一个例程集合,或多种数据类型(包括类)中的一种。如果在项目中添加一个新的空白单元,它将只包含用于划分单元节区(sections)的关键字:
unit Unit1;
interface
implementation
end.
如上所示,一个简单的单元结构包括以下要素:
- 首先,单元有一个唯一的名称与其文件名相对应(即上述示例的单元必须存储在 Unit1.pas 文件中)。
- 其次,单元有一个接口节区声明其他单元可见的内容。
- 第三,单元有一个实现节区包含实现细节、实际代码以及其他可能的局部声明,这些在单元外是不可见的。
1.4.1 单元与程序名
如前所述,单元名称必须与该单元的文件名称相对应。程序也是如此。要重命名单元,应使用项目管理器中的 "重命名 "选项,两者将保持同步(使用集成开发环境的 "另存为 "操作可使单元名和文件名保持同步,但也会在磁盘上保留旧文件)。当然,您也可以在文件系统级别更改文件名,但如果不同时更改单元开头的声明,那么在编译单元时(甚至在集成开发环境中加载单元时)就会出现错误。如果您更改了单元的声明而没有同时更新文件名,就会收到以下示例错误信息:
[DCC Error] E1038 Unit identifier 'Unit3' does not match file name
点分单元名称
单元标识符的基本规则有一个扩展:单元名称可以使用点符号(.)。因此,以下所有单元名称都是有效的:
unit1
myproject.unit1
mycompany.myproject.unit1
根据一般规则,这些单元需要保存在具有相同点号名称的文件中(也就是说,名为 MyProject.Unit1 的单元必须保存在 MyProject.Unit1.pas 文件中)。
使用这种扩展名的原因是,单元名称必须是唯一的,而随着 Embarcadero 和第三方供应商提供的单元越来越多,这一点变得越来越复杂。现在,所有 RTL 单元和作为产品库一部分提供的其他各种单元都遵循点式单元名称规则,并使用特定的前缀表示区域,例如:
- System 用于核心 RTL 单元
- 数据用于数据库访问等
- FMX 表示 FireMonkey 平台,即桌面和移动的单源多设备架构
- VCL 指 Windows 的可视化组件库
注解: 通常,在使用点式单元名称(包括库单元)时,应使用完整的名称。也可以通过在项目选项中设置相应的规则,在引用中只使用名称的最后一部分(允许与旧代码向后兼容)。该设置称为 “单元作用域名称”,是一个分号分隔的列表。不过请注意,与使用完全限定的单元名称相比,使用该功能会降低编译速度。
关于单元结构的更多内容
除了接口和实现部分,单元还可以有一个可选的初始化部分,其中包含一些启动代码,在程序首次加载到内存时执行。如果有初始化部分,也可以有终结部分,在程序终止时执行。
注解:您也可以在类构造函数中添加初始化代码,这是第 12 章中介绍的最新语言特性。使用类构造函数可以帮助链接器删除不需要的代码,这也是为什么建议使用类构造函数和类析构函数,而不是初始化和最终化部分的原因。作为历史说明,编译器仍然支持使用 begin 关键字代替初始化关键字。在项目源代码中,begin 的类似用法仍是标准用法。
换句话说,一个单元包括所有可能的部分和一些示例元素的一般结构如下所示:
unit UnitName;
interface
// 我们在接口部分引用的其他单元
uses
UnitA, UnitB, UnitC;
// 导出的类型定义
type
NewType = TypeDefinition;
// 导出的常量
const
Zero = 0;
// 全局变量
var
Total: Integer;
// 导出的函数和过程列表
procedure MyProc;
implementation
// 在实现中引用的其他单元
uses
UnitD, UnitE;
// 本地单元类型
type
NewType2 = TypeDefinition;
// 本地单元常量
const
One = 1;
// 本地单元变量
var
PartialTotal: Integer;
// 所有本地和导出的函数和过程的代码
procedure MyProc;
begin
// ... MyProc过程的代码
end;
initialization
// 可选的初始化代码
finalization
// 可选的清理代码
end.
单元接口节区的目的是详细说明单元所包含的内容,以便主程序和使用该单元的其他单元可以使用。另一方面,实现部分包含了单元的基本要素,这些基本要素是不被外部查看者发现的。因此,即使不使用类和对象,Object Pascal 也能提供所谓的封装。
正如你所看到的,单元的接口可以声明许多不同类型的元素,包括过程、函数、全局变量和数据类型。数据类型通常使用最多。每次创建可视化窗体时,集成开发环境都会自动在单元中放置一个新的类数据类型。在 Object Pascal 中,定义窗体当然不是单元的唯一用途。你可以定义只包含代码的单元,包含函数和过程(以传统的方式),以及不引用窗体或其他可视化元素的类。
在接口或实现节区中,类型、变量、常量等的声明可以按任意顺序编写,并可以重复多次。可以有几个常量、一些类型,然后是更多的常量、其他变量和另一个类型部分。唯一的规则是,如果要引用一个符号,必须在引用之前声明该符号,这也是经常需要使用多个节区的原因。
Uses子句
interface节区开头的Uses子句指示我们需要在单元的接口部分访问哪些其他单元。这包括定义我们在定义此单元数据类型中引用的数据类型的单元,例如我们正在定义的窗体中使用的组件。
第二个Uses子句位于implementation节区的开头,指示我们只需在实现代码中访问的其他单元。当您需要从例程或方法的代码中引用其他单元时,System 用于核心 RTL 单元应在第二个Uses子句中添加元素,而不是在第一个Uses子句,因为这样可以减少依赖性,缩短编译时间。您引用的所有单元都必须存在于项目目录或搜索路径目录中。
小贴士: 您可以在项目选项中为项目设置搜索路径。系统也会搜索库路径中的单元,这是集成开发环境的全局设置。
C++ 程序员应注意,uses
语句并不等同于 include
指令。uses
语句的作用只是导入所列出单元的预编译接口部分。只有在编译单元时,才会考虑单元的实现部分。您所引用的单元既可以是源代码格式(PAS
),也可以是编译格式(DCU
)。
Object Pascal
也有一个 $INCLUDE
编译器指令,尽管很少使用,但其作用类似于 C/C++
的包含。不过,这些被包含的特殊文件并不包含源代码,而是更经常被一些库用于在多个单元之间共享编译器指令或其他设置,其扩展名一般为 INC
文件。本章最后将简要介绍该指令。
警告: 请注意,只有使用相同版本的编译器和系统库构建的Object Pascal 编译单元才是兼容的。使用旧版本产品编译的单元通常与较新版本的编译器不兼容。不过,属于同一版本的更新会保持兼容性。换句话说,在 10.3.1 版本中编译的单元与所有 10.3.x 版本兼容,但与 10.2 或 10.4 版本不兼容。随着 Delphi 11 版本中版本模式的改变,任何 11.x 版本的更新都将与 11 版本保持兼容,而 12 版本则会破坏兼容性。
1.4.2 单元和作用域
在 Object Pascal 中,单元是封装和可见性的关键;从这个意义上说,单元可能比类的 private 和 public 关键字更重要。标识符(如变量、过程、函数或数据类型)的作用域是代码中标识符可访问或可见的部分。
基本规则是,标识符只有在其作用域内才有意义,也就是说,只有在声明它的单元、函数或过程中才有意义。标识符不能在超出其范围之外使用。
注解: 直到最近,Object Pascal 才有了作用域的概念,它仅限于可以包含声明的通用代码块。从 Delphi 10.3 开始,您可以在 begin-end 代码块中声明内联变量,将变量的作用域限制在特定的代码块中,就像在 C 或 C++ 中一样。更多详情请参阅第 2 章 "变量的生命周期和可见性"一节。
一般来说,标识符只有在定义后才可见。有一些语言技术允许在标识符完全定义之前对其进行声明,但如果我们同时考虑定义和声明,一般规则仍然适用。
既然在单个文件中编写整个程序意义不大,那么在使用多个单元时,上述规则又会发生怎样的变化呢?简而言之,使用 uses 语句引用另一个单元时,被引用单元的interface节区中的标识符对新单元是可见的。
反之亦然,如果在单元的interface部分声明标识符(类型、函数、类、变量等),那么任何引用该单元的其他模块都可以看到它。但是,如果在单元的实现部分声明的标识符,则只能在该单元中使用(通常称为本地标识符)。
像使用命名空间一样使用单元
我们已经看到,uses 语句是访问声明在另一个单元作用域中标识符的标准技术。此时,您可以访问单元的定义。但是,您所引用的两个单元可能声明了相同的标识符;也就是说,您可能有两个同名的类或两个同名的例程。
在这种情况下,您只需使用单元名称作为该单元中定义的类型或例程名称的前缀。例如,您可以将 Calc 单元中定义的过程 ComputeTotal 称为 Calc.ComputeTotal。这样做并不是经常需要,因为如果可以避免的话,强烈建议不要在同一程序的两个不同元素中使用相同的标识符。
不过,如果你查看系统或第三方库,就会发现有相同名称的函数和类。不同用户界面框架的可视化控件就是一个很好的例子。当你看到对 TForm 或 TControl 的引用时,它可能意味着不同的类,这取决于你引用的实际单元。
如果您的 uses 语句中的两个单元使用了相同的标识符,那么最后一个单元中的标识符将覆盖该符号,并成为编译器使用的标识符。换句话说,列表中最后一个单元中定义的符号胜出。如果无法避免这种情况,建议在符号前加上单元名称,以避免代码依赖于单元的排列顺序。
注解:Delphi 开发人员确实可以利用同名的两个类,这种技术被称为 “插入类”(interposer classes),本书稍后将解释这种技术。
程序文件
正如我们所见的那样,Delphi 应用程序由两种源代码文件组成:一个或多个单元和一个且只有一个程序文件(保存在 DPR 文件中)。单元可视为从属文件,由应用程序的主要部分(即程序)引用。从理论上讲,这是正确的。实际上,程序文件通常是自动生成的文件,作用有限。如果是可视化应用程序,它只需启动程序,一般是创建和运行主窗体。程序文件的代码可以手动编辑,但也可以通过使用集成开发环境的某些项目选项(如与应用程序对象和窗体相关的选项)来自动修改。
程序文件的结构通常比单元结构简单得多。下面是集成开发环境自动创建的示例程序文件(省略了一些可选的标准单元)的源代码:
program Project1;
uses
FMX.Forms,
Unit1 in ‘Unit1.PAS’ {Form1};
begin
Application.Initialize;
Application.CreateForm(TForm1, Form1);
Application.Run;
end.
正如你所看到的,程序中只有一个 uses 部分和主代码,由 begin 和 end 关键字括起来。程序的 uses 语句尤为重要,因为它用于管理应用程序的编译和链接。
小贴士: 程序文件中的单元列表与 IDE 项目管理器中的项目单元列表相对应。在集成开发环境中为项目添加单元时,该单元会自动添加到程序文件源代码的列表中。如果从项目中删除单元,则会发生相反的情况。无论如何,如果您编辑了程序文件的源代码,项目管理器中的单元列表也会相应更新。