Pascal 编程/枚举
Pascal 的一个强大的符号和语法工具是自定义枚举数据类型的声明。
枚举数据类型是有限的命名离散值列表。枚举实际上给单个整数值起了名字,但是,你不能(直接)对其进行算术运算。
枚举数据类型通过在数据类型标识符后面加上一个非空的逗号分隔的(新的、以前未使用过的)标识符列表来声明。
type
weekday = (Monday, Tuesday, Wednesday, Thursday, Friday,
Saturday, Sunday);
列表中的每个项目都代表数据类型可以取的特定值。数据类型标识符标识整个数据类型。
一旦枚举数据类型被声明,你就可以像使用其他任何数据类型一样使用它
var
startOfWeek: weekday;
begin
startOfWeek := Sunday;
end.
变量 startOfWeek
被限制为只能取数据类型 weekday
的合法值。请注意,Sunday
不用打字机引号 ('
) 括起来,而打字机引号通常表示字符串字面量。标识符 Sunday
表示一个本身的值。
每个枚举数据类型声明都隐式地定义了一个顺序。逗号分隔列表本质上是一个排序列表。内置函数 ord
,是序数值的缩写,使你有机会获取枚举元素的序数值,即该枚举成员的唯一的/integer
值。
枚举的第一个元素编号为 0
。第二个元素,如果有的话,编号为 1
,依此类推。
某些编译器,例如 FPC,允许你为枚举中的一些甚至所有元素指定显式索引
type
month = (January := 1, February, March, April, May, June,
July, August, September, October, November, December);
这里,January
的序数值将为 1
。所有后续项目的序数值都大于 1
。数字的自动分配仍然确保每个枚举成员在整个枚举数据类型中都有一个唯一的数字。 February
的序数值将为 2
, March
的序数值为 3
,依此类推。但是,值 0
不会分配给该枚举中的任何元素。
如果你为特定元素指定显式索引,你需要确保所有数字都是升序。你不能两次分配同一个数字。如果你跳过分配数字,自动编号系统会分配并“保留”数字。你不能使用自动系统使用的数字。 |
指定显式索引是非标准扩展。在 FPC 的 {$mode Delphi}
中,你需要使用一个简单的等号 (=
) 而不是 :=
。这也称为“C 风格枚举声明”,因为编程语言 C 使用这种语法。
Pascal 没有提供通用的函数,让你根据数字确定枚举元素。例如,没有函数可以返回 January
,如果它被提供 integer
值 1
。 [fn 1]
标准函数 pred
和 succ
,分别是前驱和后继的缩写,是自动为每个枚举数据类型定义的。这些函数返回枚举值的先前值或下一个值。例如, succ(January)
将返回 February
,因为它是值 January
的后继。
但是, pred(January)
将会失败,因为技术上不存在先于 January
的成员。枚举列表不是循环的。虽然在现实生活中,1 月份紧随 12 月份,但枚举数据类型 month
“不知道”这一点。
EP (EP) 标准允许在 pred
或 succ
中提供一个可选的第二个 integer
参数。 succ(January, 2)
等同于 succ(succ(January))
,更方便且更短,但是 pred(January, -2)
会返回相同的值。
利用此功能,您可以获取给定索引的枚举值。 succ(Monday, 3)
评估为 weekday
值,该值具有序数值 3
,从而实际上提供了一种 逆向 ord
函数的方式。但是,有必要知道枚举的第一个元素,并且枚举可能在其声明中不使用任何 显式索引(除非所有索引都与自动编号模式一致)。
枚举数据类型值自动可以与多个运算符一起使用。由于每个枚举值都具有序数值,因此可以对它们进行排序,并且您可以测试该排序。关系运算符
<
>
<=
>=
=
<>
与枚举值一起使用。例如,January < February
将评估为 true
,因为 January
的序数值小于 February
。
但是,从技术上讲,您可以比较苹果和橘子(剧透警报:它们是不等的),所有关系运算符仅与两种相同类型的值一起使用。在 Pascal 中,您不能将 weekday
值与 month
值进行比较。但是,像 ord(August) > ord(Monday)
是合法的,因为实际上您是在比较 integer
值。
请注意,算术运算符 (+
, -
, 等等) 不适用于枚举数据类型,即使它们具有序数值。
数据类型 Boolean
是一个内置的特殊枚举数据类型。保证
ord(false)
= 0,ord(true)
= 1,因此pred(true)
=false
。
Boolean
是唯一可以直接使用逻辑运算符执行操作的枚举数据类型。
最基本的运算符是否定。它是一个一元运算符,这意味着它只期望一个操作数。在 Pascal 中,它使用关键字 not
。通过在 Boolean
表达式前面加上 not
(以及一些分隔符,例如空格字符),表达式将被否定。
表达式 | 结果 |
---|---|
not true |
false
|
not false |
true
|
虽然这可能很直观,但所谓的逻辑合取(用 and
表示)可能并非如此。它的真值表如下所示
tired 的值 |
intoxicated 的值 |
tired and intoxicated 的结果 |
---|---|---|
false |
false |
false
|
false |
true |
false
|
true |
false |
false
|
true |
true |
true
|
在 EE 中,这通常写成 (“乘”)甚至省略,因为(就像数学一样)假设一个不可见的“乘”。鉴于 false
和 true
的序数值如上所述,您可以通过将它们相乘来计算 and
结果。
有点令人困惑,因为这可能与某人的自然语言相矛盾,是 or
一词。如果任一操作数是 true
,则整个表达式的结果将变为 true
。
raining 的值 |
snowing 的值 |
raining or snowing 的结果 |
---|---|---|
false |
false |
false
|
false |
true |
true
|
true |
false |
true
|
true |
true |
true
|
电子工程师经常使用 符号来表示这个操作。但是,关于 Boolean
的序数值,你必须 “定义” 仍然是 .
优先级
[edit | edit source]像数学中的常规规则 “先乘除后加减” 一样,连接运算符先于析取运算符进行求值。但是,由于否定运算符是一元运算符,所以无论如何它都会先进行求值。这意味着你必须非常小心,不要忘记放置括号。表达式
not hungry or thirsty
与
not (hungry or thirsty)
范围
[edit | edit source]序数类型
[edit | edit source]枚举数据类型属于 序数数据类型 类别。其他序数数据类型包括
整数
,字符
,- 以及所有枚举数据类型,包括
Boolean
。
它们都具有一个共同点,即它们的值可以映射到一个唯一的 integer
值。 ord
函数允许你检索该值。
区间
[edit | edit source]有时,将一组值限制在某个范围内是有意义的。例如,军事时间时钟上的小时数可能显示从 0
到包括 23
的值。但是数据类型 integer
也将允许其他值。
Pascal 允许你声明(子)范围数据类型。一个(子)范围数据类型有一个宿主数据类型,例如 integer
,以及两个限制。一个下限和一个上限。范围通过按升序给出限制,并用两个句点分隔来指定 (..
)
type
majuscule = 'A'..'Z';
限制可以作为任何可计算的表达式给出,只要它不依赖于运行时数据即可。 [fn 2] 例如,可以使用常量(已经定义的)
type
integerNonNegative = 0..maxInt;
注意,我们将此范围命名为 integerNonNegative
而不是 nonNegativeInteger
,因为这将有助于某些文档工具或 IDEs 中的字母排序。
限制
[edit | edit source]拥有一个(子)范围数据类型的变量只能取 范围内的 值。如果变量超出其合法范围,程序将中止。可能会出现以下错误消息(末尾的内存地址可能不同)
./a.out: value out of range (error #300 at 402a54)
相应的测试程序已使用 GPC 编译。其他编译器可能会发出不同的消息。
然而,FPC 的默认配置会忽略这一点。将超出范围的值分配给变量不会产生错误(如果它依赖于运行时数据)。 FPC 的开发者引用了其他编译器的兼容性原因,这些编译器为了速度原因决定忽略超出范围的值。 [fn 3] 你需要明确要求不能将非法值分配给序数类型变量。这可以通过在任何(关键)分配之前放置一个精心制作的注释来实现: {$rangeChecks on}
(不区分大小写)或 {$R+}
(区分大小写)用于简写,将确保不会分配非法值,并且如果尝试进行任何分配,程序将中止。在你的源代码文件中 一次 指定此编译器开关就足够了。 FPC 的 ‑Cr
命令行开关具有相同的效果。
范围限制必须在 存储 值时满足,例如在将值保存到变量时。然而,中间 计算(例如在计算表达式时)可能会超出范围。 |
选择
[edit | edit source]随着枚举数据类型的出现,仅仅使用 if
分支检查值可能会变得繁琐和乏味。
解释
[edit | edit source]case
选择语句将多个 互斥 的 if
分支合并为一个语言结构。 [fn 4]
case sign(x) of
-1:
begin
writeLn('You have entered a negative number.');
end;
0:
begin
writeLn('The numbered you have entered is sign-less.');
end;
1:
begin
writeLn('That is a positive number.');
end;
end;
在 case
和 of
之间,可以出现任何求值为序数值的表达式。之后, -1:
,0:
和 1:
是 case 标签。这些 case 标签标记着备选方案的开始。每个 case 标签之后是一个语句。
-1
,0
和 1
表示 case 值。每个 case 标签由一个非空的以逗号分隔的 case 值列表组成,后跟一个冒号 (:
)。所有 case 值都必须是合法的常量值,常量 表达式,它们与上面的比较表达式兼容,即 case
和 of
之间写入的内容。每个指定的 case 值需要 唯一 出现在一个 case 标签中。没有 case 标签值可以出现两次。没有必要按照它们的值进行排序,尽管这可以让你的源代码更易读。
在 EP 中,case 标签可以包含范围。
program letterReport(input, output);
var
c: char;
begin
write('Give me a letter: ');
readLn(c);
case c of
'A'..'Z':
begin
writeLn('Wow! That’s a big letter!');
end;
'a'..'z':
begin
writeLn('What a nice small letter.');
end;
end;
end.
这种简写符号允许你捕获许多情况。case 标签 'A'..'Z':
包含所有大写字母,无需单独列出它们。
注意,任何范围都不应与其他 case 标签值重叠。这是禁止的。好的处理器会对这种错误提出警告。 GPC 会给出错误信息 duplicate case-constant in `case' statement
,FPC 会报告 duplicate case label
[fn 5],两者都会提供一些有关你源代码中位置的信息。
重要的是,比较表达式的任何(预期)值都应匹配一个 case 标签。如果比较表达式计算出的值不在任何 case 标签包含的值范围内,程序将中止。[fn 6] 如果不希望出现这种情况,“扩展 Pascal”标准允许使用一个名为 otherwise
(注意,没有冒号)的特殊 case 标签。此 case 会处理所有没有与之关联的显式 case 标签的值。
program asciiTest(input, output);
var
c: char;
begin
write('Supply any character: ');
readLn(c);
case c of
// empty statement, so the control characters are not
// considered by the otherwise-branch as non-ASCII characters
#0..#31, #127: ;
#32..#126:
begin
writeLn('You entered an ASCII printable character.');
end;
otherwise
begin
writeLn('You entered a non-ASCII character.');
end;
end;
end.
otherwise
只能在末尾出现。之前必须至少有一个 case 标签,否则(双关语) otherwise
case 将始终被执行,从而导致整个 case
语句失效。
BP(即 Delphi)重新使用单词 else
,其语义与 otherwise
相同。 FPC 和 GPC 都支持两者,尽管 GPC 可以被指示只接受 otherwise
。
function
,返回 month
的后继,但对于 December
,返回的值为 January
。case
语句非常适合function successor(start: month): month;
begin
case start of
January..November:
begin
successor := succ(start);
end;
December:
begin
successor := January;
end;
end;
end;
出于本练习的目的(演示关系运算符如 <
是如何自动为枚举数据类型定义的),以下也是可以接受的
function successor(start: month): month;
begin
if start < December then
begin
successor := succ(start);
end
else
begin
successor := January;
end;
end;
然而,从数学角度讲,case
实现更加精确。在第一个实现中,如果参数错误、超出范围,程序将中止。
if … then … else
的第二个实现也会对非法值“错误地”定义。
hasRained 的值 |
streetWet 的值 |
的结果 |
---|---|---|
false |
false |
true
|
false |
true |
true
|
true |
false |
false
|
true |
true |
true
|
Boolean
表达式,使其得到相同的真值。假设 hasRained
和 streetWet
是 Boolean
变量;你将如何将它们关联起来,以便整个 Boolean
表达式与数学表达式 相同?Boolean
是一个内置枚举数据类型。这意味着它是有序的,因此这种数据类型的成员可以按关系顺序排列。在编程中,你遇到的 的最常见翻译是not hasRained or streetWet
hasRained <= streetWet
Boolean
是一个枚举数据类型。然而,一些没有使用帕斯卡语言编程的人 (例如,编写个人文本消息) 可能使用 <=
来表示 ,这与 正好相反 (这意味着在数学中,,即交换了 和 ,是另一种同样有效的表示 的方式)。如果你属于这一类人,你可能会发现这种简短的表达方式不直观,因为在帕斯卡语言中,<=
事实上是 ≤ (小于或等于),而不是 ⇐。注释
- ↑ 一些编译器,例如 FPC,允许“类型转换”,实际上将
1
转换为January
。然而,这种类型转换不是一个函数,尤其是当值超出范围时,类型转换无法正常工作 (不会产生运行时错误,也不会采取其他措施)。 - ↑ 这是“扩展帕斯卡” (ISO 10206) 的扩展。在标准的“非扩展”帕斯卡 (ISO 7185) 中,只允许使用常量。
- ↑ 帕斯卡 ISO 标准确实允许这样做。编译器程序员可以选择忽略这些错误。但是,附带的文档 (手册等) 应该指出这一点。
- ↑ 这是一个类比。
case
语句通常不会被转换为一系列if
分支。 - ↑ 这个错误消息是不准确的。GNU Pascal 编译器的错误消息更准确。问题在于某个值“case-constant”出现了多次。
- ↑ 许多编译器在其默认配置中不遵守此要求。GNU Pascal 编译器需要被指示为“完全”符合 ISO 标准 (
‑‑classic‑pascal
,‑‑extended‑pascal
或仅‑‑case‑value‑checking
)。在 BP 中,Delphi 将会继续执行,并忽略缺失的 case。截至 3.2.0 版本,FreePascal 编译器完全不考虑此要求。