Object Pascal 手册,Delphi 11 编程语言的完整介绍 作者: Marco Cantu 笔记:豆豆爸
? 除了类型的概念,Niklaus Wirth 在最初的 Pascal 语言中引入的一个伟大想法是在程序中定义新的数据类型(我们今天认为这是理所当然的,但在当时并不明显)。你可以通过类型定义的方式定义自己的数据类型,如子界类型、数组类型、记录类型、枚举类型、指针类型和集合类型。类是最重要的用户自定义数据类型,它是面向对象语言功能的一部分,将在本书第二部分介绍。
? 如果你认为类型构造函数在许多编程语言中都很常见,那么你是对的,但 Pascal 是第一种以正式和非常精确的方式引入这种思想的语言。Object Pascal 仍有一些相当独特的功能,比如子界、枚举和集合的定义,这些将在下面的章节中介绍。第 5 章将介绍更复杂的数据类型构造函数(如数组和记录)。
? 用户定义的数据类型可以为以后使用的变量命名,也可以直接应用于变量。Object Pascal 的惯例是使用字母 T 前缀来表示任何数据类型,包括类但不限于类。我强烈建议你遵守这一规则,如果你来自 Java 或 C# 背景,即使一开始可能感觉不自然。
? 为类型命名时,必须在程序的 "type"部分进行(每个单元中可以添加任意多的类型)。下面是几个类型声明的简单示例:
type
// 子界定义
TUppercase = 'A'..'Z';
// 枚举类型定义
TMyColor = (Red, Yellow, Green, Cyan, Blue, Violet);
// 集合定义
TColorPalette = set of TMyColor;
? 有了这些类型,现在您就可以定义一些变量:
var
UpSet: TUpperLetters;
Color1: TMyColor;
? 在上述情况中,我使用的是命名类型。另一种方法是直接使用类型定义来定义变量,而不使用明确的类型名称,如下面的代码:
var
Palette: set of TMyColor;
? 一般来说,应避免使用上述代码中的未命名类型,因为您不能将它们作为参数传递给例程或声明相同类型的其他变量。鉴于语言最终采用的是类型名称等价而非结构类型等价,因此对每种类型使用单个定义确实很重要。另外请记住,通过 uses 语句,可以在任何其他单元的代码中看到单元interface部分的类型定义。
? 上述类型定义是什么意思?我将为那些不熟悉传统 Pascal 类型结构的人提供一些说明。我还会尽量强调与其他编程语言中相同结构的差异,所以无论如何,你都可能有兴趣阅读下面的章节。
? 正如我们所看到的,Delphi 语言在检查类型兼容性时使用的是类型名称(而不是实际定义)。两个定义相同但名称不同的类型就是两个不同的类型。
? 在定义类型别名(即基于现有类型的新类型名称)时,部分情况也是如此。令人困惑的是,同样的语法有两种变体,产生的效果却略有不同。请看 TypeAlias
示例中的代码:
type
TNewInt = Integer;
TNewInt2 = type Integer;
? 这两种新类型与 Integer 类型保持赋值兼容(通过自动转换);但是,TNewInt2 类型不会完全匹配,例如,它不能作为引用参数传递给期望使用别名类型的函数:
procedure Test(var N: Integer);
begin
end;
procedure TForm40.Button1Click(Sender: TObject);
var
I: Integer;
NI: TNewInt;
NI2: TNewInt2;
begin
I := 10;
NI := I; // 正常工作
NI2 := I; // 正常工作
Test(I);
Test(NI);
Test(NI2); // 错误
? 最后一行会产生错误:
E2033 Types of actual and formal var parameters must be identical
? 与类型助手一样,Integer 类型助手可用于 TNewInt,但不能用于 TNewInt2。这在后面讨论记录助手时有具体涉及。
? 子范围(subrange)类型定义了另一个类型范围内的值范围(因此称为子范围)。例如,您可以定义一个从1到10或从100到1000的字符类型的子范围,或者您可以定义一个只包含英文字符的字符类型的子范围,如下所示:
type
TTen = 1..10;
TOverHundred = 100..1000;
TUpperCase = 'A'..'Z';
? 在子范围的定义中,您无需指定基本类型的名称。您只需提供该类型的两个常量。原始类型必须是序数类型,结果类型将是另一种序数类型。当您将变量定义为子范围时,然后可以将任何在该范围内的值分配给它。以下代码是有效的:
var
UppLetter: TUpperCase;
begin
UppLetter := 'F';
? 但这是无效的:
var
UppLetter: TUpperCase;
begin
UppLetter := 'e'; // 编译时错误
? 上述代码会导致编译错误,“常量表达式违反子范围边界”。如果您改为编写以下代码,编译器将通过编译:
var
UppLetter: TUpperCase;
Letter: Char;
begin
Letter :='e';
UppLetter := Letter;
end;
? 在运行时,如果启用了范围检查编译器选项(在 "项目选项 "对话框的 "编译器 "页面),就会如预期一样收到范围检查错误信息。这与我前面描述的整数类型溢出错误类似。
? 我建议你在开发程序时打开这个编译器选项,这样程序会更健壮,调试起来也更容易,因为如果出现错误,你会得到一个明确的信息,而不是一个不确定的行为。在最终编译程序时,你可以禁用该选项,这样程序运行速度会更快一些。不过 速度的提高几乎可以忽略不计,因此我建议将所有这些运行时检查都打开、 即使是在运行中的程序中。
? 枚举类型(通常称为 “枚举”)是另一种用户定义的序数类型。在枚举类型中,用户列出的不是现有类型的范围,而是该类型的所有可能值。换句话说,枚举是一个(常量)值列表。下面是一些示例:
type
TColors = (Red, Yellow, Green, Cyan, Blue, Violet);
TSuit = (Club, Diamond, Heart, Spade);
? 列表中的每个值都有一个相关的常量,从 0 开始。对枚举类型的值应用 Ord 函数时,就会得到这个 "基于零 "的值。例如,Ord(Diamond) 返回 1。
? 枚举类型可以有不同的内部表示法。默认情况下,Delphi 使用 8 位表示法,除非有超过 256 个不同的值,在这种情况下使用 16 位表示法。此外还有 32 位表示法,有时这种表示法对于与 C 或 C++ 库兼容非常有用。
注解: 您可以通过使用
$Z
编译器指令来更改枚举类型的默认表示法,即无论枚举中的元素数量多少,都要求使用较大的表示法。这是一种相当罕见的设置。
域枚举
? 枚举类型的特定常量值可以被视为全局常量,不同的枚举值之间可能存在命名冲突的情况。这就是语言支持作用域枚举的原因,这是一种您可以使用特定编译器指令 $SCOPEDENUMS
来激活的功能,并且需要您使用类型名称作为前缀来引用枚举值:
// 经典的枚举值
S1 := Club;
// 作用域枚举值
S1 := TSuit.Club;
? 当引入该功能时,为了避免破坏现有代码,保留了传统的默认编码样式。实际上,域枚举改变了枚举的行为,使其必须使用完全限定的类型前缀来引用。
? 拥有一个绝对名称来引用枚举值消除了冲突的风险,可以避免只使用枚举的初始前缀从而与其他枚举值区分开,这样即使写起来更长,也使代码更易读。
? 例如,System.IOUtils
单元定义了此类型:
{$SCOPEDENUMS ON}
type
TSearchOption = (soTopDirectoryOnly, soAllDirectories);
? 这意味着您不能将第二个值称为 soAllDirectories
,而必须使用其完整名称引用枚举值:
TSearchOption.soAllDirectories
? FireMonkey 平台库也使用了大量域枚举,要求将类型作为实际值的前缀,而较早的 VCL 库一般采用更传统的模式。RTL 是采用了两者的混合体。
注解: Object Pascal 库中的枚举值通常在值的开头使用两个或三个类型的首字母,按惯例使用小写字母,如上例中的搜索选项(Search Options)的 “so”。在使用类型作为前缀时,这似乎有点多余,但考虑到这种方法的普遍性,我认为它不会很快消失。
? 集合类型表示一组值,可用值的列表由集合所基于的序数类型指示。这些序数类型通常是有限的,通常用枚举或子界来表示。
? 如果我们让子界取值为 1 到 3,用 Pascal 符号写成 1…3,那么基于它的集合的可能取值包括只有 1、只有 2、只有 3、既有 1 又有 2、既有 1 又有 3、既有 2 又有 3、所有三个值或一个值都没有。
? 变量通常包含其类型范围的可能值之一。另一方面,集合类型变量可以不包含任何值,也可以包含一个、两个、三个或任意多个范围内的可能值。它甚至可以包含所有的值。
? 下面是一个集合的示例:
type
TSuit = (Club, Diamond, Heart, Spade);
TSuits = set of TSuit;
? 现在,我可以定义一个这种类型的变量,并为其分配一些原始类型的值。要表示集合中的某些值,可以写一个用逗号分隔的列表,并用方括号括起来。下面的代码显示了给变量赋值的情况,包括几个值、一个值和一个空值:
var
Cards1, Cards2, Cards3: TSuits;
begin
Cards1 := [Club, Diamond, Heart];
Cards2 := [Diamond];
Cards3 := [];
? 在 Object Pascal
中,集合通常用于指示多个非排他性标志。例如,基于集类型的值是字体样式。可能的值表示粗体、斜体、下划线和删除线字体。当然,相同的字体可以是斜体和粗体,没有属性,也可以具有全部属性。因此,它被声明为一个集合。
? 您可以在程序代码中为该集合赋值,具体方法如下:
Font.Style := []; // No style
Font.Style := [fsBold]; // Bold style only
Font.Style := [fsBold, fsItalic]; // Two styles active
集合运算符
? 我们已经看到,集合是一种非常具有Pascal
风格的用户自定义数据类型。这就是为什么集合运算符值得特别介绍的原因。它们包括并(+)、差(-)、交(*)、成员测试(in)以及一些关系运算符。
? 要将元素添加到集合中,可以将该集合与另一个仅包含所需元素的集合合并。下面是一个与字体样式相关的示例:
// Add bold
Style := Style + [fsBold];
// Add bold and italic, but remove underline if present
Style := Style + [fsBold, fsItalic] - [fsUnderline];
? 作为替代方法,您可以使用标准的Include和Including过程,这要有效得多(但不能与set类型的组件属性一起使用):
Include(Style, fsBold);
Exclude(Style, fsItalic);