Pascal 编程/记录
成功编程的关键是找到数据的“正确”结构和程序。
—尼克劳斯·维尔特[1]
在你学会使用 array
之后,本章介绍了另一种数据类型结构概念,称为 record
。像 array
一样,使用 record
的主要目的是允许你编写干净的、结构化的 程序。否则它是可选的。
概念
[edit | edit source]你在 第一章 中简要地看到了 record
。虽然 array
是同质的数据聚合,这意味着所有成员都必须具有相同的基本数据类型,但 record
可能,但并非一定是具有各种不同数据类型的聚合。 [2]
声明
[edit | edit source]一个 record
数据类型声明看起来很像一个集合的变量声明
program recordDemo;
type
(* a standard line on a text console *)
line = string(80);
(* 1st grade through 12th grade *)
grade = 1..12;
(* encapsulate all administrative data in one structure *)
student = record
firstname: line;
lastname: line;
level: grade;
end;
声明以 record
一词开始,并以 end
结束。在两者之间,你声明了字段,或成员,整个 record
的成员元素。
这里分号的作用是分隔成员。关键字 end 实际上将终止一个 record 声明。请注意,在以下正确示例中,最后一个成员声明后面没有分号program recordSemicolonDemo;
type
sphere = record
radius: real;
volume: real;
surface: real
end;
|
|
所有 record
成员必须在 record
声明本身内具有不同的名称。例如,在上面的示例中,声明两个名为 level
的“变量”,成员元素将被拒绝。
对要声明的字段数量没有要求。一个“空” record
也是可能的:[fn 1]
type
emptyRecord = record
end;
多个相同数据类型的字段
[edit | edit source]与 变量声明 类似,你可以通过用逗号分隔标识符来定义多个相同数据类型的字段。之前的 sphere
声明也可以写成
type
sphere = record
radius, volume, surface: real;
end;
然而,大多数 Pascal 老手和风格指南不鼓励使用这种简写符号(用于变量和 record
声明,以及在形式参数列表中)。它只有在所有声明的标识符绝对始终具有相同数据类型时才合理;几乎可以保证你永远不想改变逗号分隔列表中仅一个字段的数据类型。如有疑问,请使用长格式。在编程中,便利起着间接的作用。
使用
[edit | edit source]通过声明一个 record
变量,你立即获得了整个集合的“子”变量。访问它们是通过指定record
变量的名称,加上一个点 (.
),然后是 record
字段的名称
var
posterStudent: student;
begin
posterStudent.firstname := 'Holden';
posterStudent.lastname := 'Caulfield';
posterStudent.level := 10;
end.
你已经在上一章关于 字符串 中看到了点符号,其中在 .capacity
上附加一个 string(…)
变量的名称指的是相应变量的字符容量。这不是巧合。
program dotNoGo(output); { This program does not compile. }
type
line = string(80);
quizItem = record
question: line;
answer: line;
end;
var
response: line;
challenge: quizItem;
begin
writeLn(line.capacity); { ↯ `line` is not a variable }
writeLn(response.capacity); { ✔ correct }
writeLn(quizItem.question); { ↯ `quizItem` refers to a data type }
{ Data type declarations (as per definition) do not reserve any memory }
{ thus you cannot “read/write” from/to a data type. }
writeLn(challenge.question); { ✔ correct }
end.
.
) 只有在有内存的情况下才有效。 [fn 2]
优点
[edit | edit source]但是为什么以及何时我们想要使用 record
?乍一看,在给出的示例中,它似乎是声明和使用多个变量的麻烦方式。然而, record
被作为一个单位处理这一事实包含一个很大的优势
- 您可以通过简单的赋值(
:=
)来复制整个record
的值。 - 这意味着您可以一次传递大量数据:一个
record
可以作为例程的参数,在EP函数中也可以作为返回值。[fn 3]
显然,您希望将始终一起出现的数据分组。将无关的数据分组没有意义,仅仅因为我们可以这样做。另一个非常有用的优点将在下面关于变体记录部分中介绍。
正如您之前看到的,引用record
的成员可能会有点繁琐,因为我们一遍又一遍地重复变量名。幸运的是,Pascal 允许我们稍微缩写一下。
with
-子句允许我们消除重复公共前缀,特别是record
变量的名称。[3]
begin
with posterStudent do
begin
firstname := 'Holden';
lastname := 'Caulfield';
level := 10;
end;
end.
所有标识值的标识符首先在record
的posterStudent
范围内进行查找。如果找不到匹配项,则也会考虑给定record
外部的所有变量标识符。
当然,仍然可以使用完整名称来表示record
成员。例如,在上面的源代码中,在with
-子句内仍然可以完全合法地写posterStudent.level
。诚然,这会违背with
-子句的目的,但有时为了文档目的,强调特定的record
变量可能仍然是有益的。但是,重要的是要理解,FQI(完全限定标识符),即带有一个点的标识符,不会失去其“有效性”。
原则上,所有包含“点”的结构化值组件都可以用with
来缩写。这也适用于你在上一章学过的数据类型string
。
program withDemo(input, output);
type
{ Deutsche Post „Maxi-Telegramm“ }
telegram = string(480);
var
post: telegram;
begin
with post do
begin
writeLn('Enter your telegram. ',
'Maximum length = ',
capacity, ' characters.');
readLn(post);
{ … }
end;
end.
这里,在with
-子句capacity
内,同样在post.capacity
内,都指的是post.capacity
。
如果需要嵌套多个with
-子句,可以使用简短的表示法
with snakeOil, sharpTools do
begin
…
end;
它等效于
with snakeOil do
begin
with sharpTools do
begin
…
end;
end;
重要的是要牢记,首先在sharpTools
中搜索标识符,如果找不到匹配项,其次,考虑snakeOil
中的标识符。
在 Pascal 中,record
是唯一允许您在运行时(在program
运行时)改变其结构的数据类型结构概念。这种record
的超实用属性允许我们编写涵盖许多情况的通用代码。
让我们看一个例子
type
centimeter = 10..199;
// order of female, male has been chosen, so `ord(sex)`
// returns the [minimum] number of non-defective Y chromosomes
sex = (female, male)
// measurements according EN 13402 size designation of clothes [incomplete]
clothingSize = record
shoulderWidth: centimeter;
armLength: centimeter;
bustGirth: centimeter;
waistSize: centimeter;
hipMeasurement: centimeter;
case body: sex of
female: (
underbustMeasure: centimeter;
);
male: (
);
end;
record
的变体部分以关键字case
开头,您已经从选择中了解过。之后跟着一个record
成员声明,变体选择器,但您不要使用分号,而是使用关键字of
。在此之后,所有可能的变体都在下面。每个变体都用变体选择器域中的一个值来标记,这里分别是female
和male
。用冒号(:
)分隔,之后跟着用括号括起来的变体指示符。在这里,您可以列出只有在某个变体“激活”时才可用的其他record
成员。请注意,所有变体中的所有标识符都必须是唯一的。各个变体用分号分隔,最多可以有一个变体部分,它必须出现在最后。因为您需要能够列出所有可能的变体,所以变体选择器必须是序数数据类型。
使用变体记录要求您首先选择一个变体。通过将一个值赋给变体选择器来“激活”变体。请注意,变体不是“创建”的;它们都在program
启动时就已存在。您只需要做出选择。
boobarella.body := female;
boobarella.underbustMeasure := 69;
只有在将值赋给变体选择器之后,并且只要该值保持不变,您才能访问相应变体的任何字段。反转前两行代码并尝试访问underbustMeasure
字段是非法的,即使body
尚未定义,更重要的是,它不具有值female
。
在您的program
中稍后更改变体选择器是完全可以的,然后使用不同的变体,但是变体部分中所有先前存储的值都将失效,您无法恢复它们。如果您将变体切换回先前,原始的值,则需要重新分配该变体中的所有值。
这个概念开辟了新的视野:您可以以简洁的方式更交互地设计您的程序。现在,您可以根据运行时数据(在program
运行时读取的数据)来选择变体。因为在任何时候(在第一次将值赋给变体选择器之后),只有一个变体是“激活”的,所以如果您的program
尝试读取/写入“非激活”变体的值,它就会崩溃。这是期望的行为,因为这就是拥有不同变体的目的。它保证了您的程序整体完整性。
Pascal 还允许使用匿名变体选择器,即不带任何名称的选择器。其含义是
- 您无法显式选择(或查询)任何变体,因此
- 反过来,所有变体都被认为是同时“激活”的。
“但是,这不是练习的目的吗?”你可能会问。是的,确实,因为没有命名选择器,你的 program
无法跟踪哪个变体应该工作,哪个变体是“有缺陷的”。你 有责任确定目前你可以合理地读/写哪个变体。
program anonymousVariantsDemo(output);
type
bitIndex = 0..(sizeOf(integer) * 8 - 1);
exposedInteger = record
case Boolean of
false: (
value: integer;
);
true: (
bit: set of bitIndex;
);
end;
var
i: exposedInteger;
begin
i.bit := [4];
writeLn(i.value);
end.
16
16
是(这应该被认为是“巧合”). 我们强调,所有 Pascal 标准都没有对内部内存结构做出任何声明。高级编程语言不关心数据如何存储,它甚至不知道“位”,“高电压”/“低电压”的概念。因此,如果你(有意地)使用这里演示的任何行为,你不能再说是“我在用 Pascal 编程”,而是在专门针对编译器如此这般进行编程。数据结构的内存布局在 Pascal 实现之间有所不同。 |
这个概念也存在于许多其他编程语言中。例如,在编程语言 C 中,它被称为 联合。
条件循环
[edit | edit source]到目前为止,我们一直只使用计数 循环。如果你可以提前预测迭代次数,循环体需要执行多少次,这将非常棒。但很多时候,无法事先制定一个适当的表达式来确定迭代次数。
条件循环 允许你让下一次迭代的执行取决于一个Boolean
表达式。它们有两种形式
- 头部控制循环,以及
- 尾部控制循环。
区别在于,尾部控制循环的循环体至少执行一次,而头部控制循环可能根本不执行循环体。在任何情况下,都会反复评估一个条件,并且必须保持该条件才能使循环继续。
头部控制循环
[edit | edit source]头部控制循环通常被称为 while
循环,因为它的语法。
program characterCount(input, output);
type
integerNonNegative = 0..maxInt;
var
c: char;
n: integerNonNegative;
begin
n := 0;
while not EOF do
begin
read(c);
n := n + 1;
end;
writeLn('There are ', n:1, ' characters.');
end.
$ cat ./characterCount.pas | ./characterCount
There are 240 characters.
$ printf '' '' | ./characterCount
There are 0 characters.
Boolean
表达式,用 while
和 do
两个词框起来。对于任何(后续)迭代,该条件必须评估为 true
。从输出中可以看出,在第二种情况下,它甚至可能为零次:显然,对于空输入,n := n + 1
从未执行。EOF
是 EOF(input)
的简写。这个标准 function
如果没有更多数据可供读取,则返回 true
,通常称为文件结束。如果相应的 EOF
函数调用返回 true
,则从文件中 read
是非法的,并且会非常糟糕地失败。
与计数循环不同,你可以修改条件循环的条件所依赖的数据。
const
(* instead of a hard-coded length `64` *)
(* you can write `sizeOf(integer) * 8` in Delphi, FPC, GPC *)
wordWidth = 64;
type
integerNonNegative = 0..maxInt;
wordStringIndex = 1..wordWidth;
wordString = array[wordStringIndex] of char;
function binaryString(n: integerNonNegative): wordString;
var
(* temporary result *)
binary: wordString;
i: wordStringIndex;
begin
(* initialize `binary` with blanks *)
for i := 1 to wordWidth do
begin
binary[i] := ' ';
end;
(* if n _is_ zero, the loop's body won't be executed *)
binary[i] := '0';
(* reverse Horner's scheme *)
while n >= 1 do
begin
binary[i] := chr(ord('0') + n mod 2);
n := n div 2;
i := i - 1;
end;
binaryString := binary;
end;
循环条件所依赖的 n
将被反复除以二。由于除法运算符是整数除法 (div
),因此在某个时候,值 1
将被除以二,并且算术上正确的结果 0.5
被截断 (trunc
) 到零。但是,值 0
不再满足循环的条件,因此将不会有任何后续迭代。
尾部控制循环
[edit | edit source]在尾部控制循环中,条件出现在循环体下方,在尾部。循环体始终在条件评估之前被运行一次。
program repeatDemo(input, output);
var
i: integer;
begin
repeat
begin
write('Enter a positive number: ');
readLn(i);
end
until i > 0;
writeLn('Wow! ', i:1, ' is a quite positive number.');
end.
循环体被 repeat
和 until
关键字封装起来。[fn 4] 在 until
后面跟一个 Boolean
表达式。与 while
循环相反,尾部控制循环始终继续,始终保持运行,until
指定的条件变为 true
。一个 true
条件表示结束。在上面的示例中,用户将被反复提示,直到他最终服从并输入一个正数。
日期和时间
[edit | edit source]本节向你介绍 ISO 标准 10206 中定义的扩展 Pascal 的功能。你需要一个符合 EP 的编译器才能使用这些功能。
时间戳
[edit | edit source]在 EP 中,有一个名为 timeStamp
的标准数据类型。它被声明如下:[fn 5]
type
timeStamp = record
dateValid: Boolean;
timeValid: Boolean;
year: integer;
month: 1..12;
day: 1..31;
hour: 0..23;
minute: 0..59;
second: 0..59;
end;
从声明中可以看出,timeStamp
还包含用于日历日期的数据字段,而不仅仅是标准时钟指示的时间。
处理器(即通常是编译器)可能会提供额外的(因此是非标准的)字段。例如,GPC 提供了包括 timeZone 在内的其他字段,该字段指示相对于 UTC(“世界时间”)的秒数偏移量。 |
获取时间戳
[edit | edit source]EP 还定义了一个一元的 procedure
,它将值填充到 timeStamp
变量中。 GetTimeStamp
将值分配给传递到第一个(也是唯一)参数中的 timeStamp record
的所有成员。这些值代表调用此过程时的“当前日期”和“当前时间”。但是,在 1980 年代,并非所有(个人/家庭)计算机都具有内置的“实时”时钟。因此,ISO 标准 10206 在 21 世纪之前制定,指出“当前”一词是“实现定义的”。dateValid
和 timeValid
字段专门用于解决某些计算机根本不知道当前日期和/或时间的问题。从 timeStamp
变量中读取值时,在让 getTimeStamp
填充它们之后,仍建议先检查其有效性。
如果 getTimeStamp
无法获得“有效”值,它将设置
day
、month
和year
为表示 公元 1 年 1 月 1 日 的值,但同时也将dateValid
设置为false
。- 对于时间,
hour
、minute
和second
都会变为0
,表示午夜的值。timeValid
字段变为false
。
两者相互独立,因此完全有可能只确定时间,但日期无效。
请注意,公历是在公元 1582 年引入的,因此 timeStamp
数据类型通常对公元 1583 年之前的任何日期都无用。
获得 timeStamp
后,EP 还会提供两个一元函数
date
返回day
、month
和year
的人类可读的string
表示形式,以及time
返回hour
、minute
和second
的人类可读的string
表示形式。
如果 dateValid
或 timeValid
分别指示数据无效,则这两个函数都将失败并终止 program
。请注意,string
表示形式的确切格式未由 ISO 标准 10206 定义。
program
program dateAndTimeFun(output);
var
ts: timeStamp;
begin
getTimeStamp(ts);
if ts.dateValid then
begin
writeLn('Today is ', date(ts), '.');
end;
if ts.timeValid then
begin
writeLn('Now it is ', time(ts), '.');
end;
end.
Today is 10 Oct 2024. Now it is 13:42:42.
dateValid
和 timeValid
都为 false
。现在是盘点并重申各种循环的好时机。
如果你无法预测总迭代次数,条件循环是首选工具。
头部控制循环 | 尾部控制循环 |
---|---|
while condition do
begin
…
end;
|
repeat
begin
…
end
until condition;
|
condition 必须评估为 true 才能发生任何(包括后续)迭代。 |
condition 必须为 false 才能发生任何后续迭代。 |
可以将任一循环表示为另一个循环,但通常其中一个更合适。尾部控制循环特别适合在还没有任何数据可以判断的情况下使用,以便在第一次迭代之前评估合适的 condition
。
如果你可以在进入循环之前预测总迭代次数,则计数循环很合适。
向上计数循环 | 向下计数循环 |
---|---|
for controlVariable := initialValue to finalValue do
begin
…
end;
|
for controlVariable := initialValue downto finalValue do
begin
…
end;
|
在每次非最终迭代之后,controlVariable 都会变为 succ(controlVariable) 。 controlVariable 必须小于或等于 finalValue 才能发生另一次迭代。 |
在每次非最终迭代之后,controlVariable 都会变为 pred(controlVariable) 。 controlVariable 必须大于或等于 finalValue 才能发生另一次迭代。 |
initialValue 和 finalValue 表达式都会被精确评估一次。 [4] 这与条件循环有很大不同。 |
在计数循环的循环体内,你不能修改计数变量,只能读取它。这可以防止任何意外操作,并确保计算的预测总迭代次数确实会发生。
不能保证 controlVariable 在循环“之后”为 finalValue 。如果有正好零次迭代,则对 controlVariable 不会进行任何赋值。因此,通常假定 controlVariable 在 for 循环之后无效/未初始化,除非你绝对确定至少进行了一次迭代。 |
如果你使用的是支持 EP 的编译器,还可以选择对集合使用 for … in
循环。
program forInDemo(output);
type
characters = set of char;
var
c: char;
parties: characters;
begin
parties := ['R', 'D'];
for c in parties do
begin
write(c:2);
end;
writeLn;
end.
你已经走到这一步了,你所知道的知识已经相当令人印象深刻。由于本章关于 record
的概念应该不难理解,以下练习主要侧重于训练。一个专业的计算机程序员大部分时间都花在思考什么样的实现,使用哪些工具(例如 array
“vs.” set
),是最有用/合理的。鼓励你在开始输入任何内容之前先思考。尽管如此,有时(尤其是由于你缺乏经验)你需要尝试一下,如果这是有意的,那就没关系。漫无目的地找到解决方案并不能体现真正的程序员。
record
可以包含另一个 record
吗?array
可以包含另一个 array
一样,record
也完全可以做到。写一个测试 program
来验证这一点。重要的是要注意,点符号可以无限扩展(myRecordVariable.topRecordFieldName.nestedRecordFieldName.doubleNestedRecordFieldName
)。显然,在某个时候它变得难以阅读,因此请明智地使用它。
while true do
begin
…
end;
在 repeat … until
循环 中需要否定条件
repeat
begin
…
end
until false;
true
的表达式,或者永远无法满足的表达式(在 repeat … until
循环 的情况下),则不然。例如,假设 i
是一个 integer
,则循环 while i <= maxInt do
将无限期地运行,因为 i
永远不会超过 maxInt
[fn 6],从而破坏循环条件。因此,请记住仔细为条件循环制定表达式,并确保它最终会达到终止状态。否则,这可能会让你的 program
的用户感到沮丧。
while
循环repeat
begin
imagineJumpingSheep;
sheepCount := sheepCount + 1;
waitTwoSeconds;
end
until asleep;
while
循环开始之前就被重复了imagineJumpingSheep;
sheepCount := sheepCount + 1;
waitTwoSeconds;
while not asleep do
begin
imagineJumpingSheep;
sheepCount := sheepCount + 1;
waitTwoSeconds;
end;
repeat … until
循环 在这种情况下更合适。
program
,它将命令 getent passwd
的输出作为输入,并且只打印每行中的第一个字段/列。在 文件中,字段用冒号 (:
) 分隔。你的 program
将列出所有已知的用户名。getent passwd | ./cut1
运行以下程序(你的可执行程序的文件名可能不同)。program cut1(input, output);
const
separator = ':';
var
line: string(80);
begin
while not EOF(input) do
begin
{ This reads the _complete_ line, but at most}
{ line.capacity characters are actually saved. }
readLn(line);
writeLn(line[1..index(line, separator)-1]);
end;
end.
index
将返回冒号字符的索引,你不想打印它,因此你需要从它的结果中减去 1
。如果一行不包含冒号,则此 program
显然会失败。
program
,以便仅打印 UID 大于或等于 1000
的用户名。UID 存储在第三个字段中。program cut2(input, output);
const
separator = ':';
minimumID = 1000;
var
line: string(80);
nameFinalCharacter: integer;
uid: integer;
begin
while not EOF do
begin
readLn(line);
nameFinalCharacter := index(line, separator) - 1;
{ username:encryptedpassword:usernumber:… }
{ ↑ `nameFinalCharacter + 1` }
{ ↑ `… + 2` is the index of the 1st password character }
uid := index(subStr(line, nameFinalCharacter + 2), separator);
{ Note that the preceding `index` did not operate on `line` }
{ but an altered/different/independent “copy” of it. }
{ This means, we’ll need to offset the returned index once again. }
readStr(subStr(line, nameFinalCharacter + 2 + uid), uid);
{ Read/readLn/readStr automatically terminate reading an integer }
{ number from the source if a non-digit character is encountered. }
{ (Preceding blanks/space characters are ignored and }
{ the _first_ character still may be a sign, that is `+` or `-`.)}
if uid >= minimumID then
begin
writeLn(line[1..nameFinalCharacter]);
end;
end;
end.
subStr
中的第三个参数可以省略,这实际上意味着“给我一个 string
的剩余部分”。注意,此编程任务模拟了 的(部分)行为。使用已经为你编程的程序/源代码,只要有可能。没有必要重新发明轮子。尽管如此,这个基本任务是一个很好的练习。在 RHEL 系统上,你可能更希望将 minimumID
设置为 500
。
program
满足所有要求。注意,使用 array[1..limit] of Boolean
的实现也完全没问题,尽管所示的 set of natural
实现原则上是首选的。program eratosthenes(output);
type
{ in Delphi or FPC you will need to write 1..255 }
natural = 1..4095;
{$setLimit 4096}{ only in GPC }
naturals = set of natural;
const
{ `high` is a Borland Pascal (BP) extension. }
{ It is available in Delphi, FPC and GPC. }
limit = high(natural);
{ Note: It is important that `primes` is declared }
{ in front of `sieve` and `list`, so both of these }
{ routines can access the _same_ variable. }
var
primes: naturals;
{ This procedure sieves the `primes` set. }
{ The `primes` set needs to be fully populated }
{ _before_ calling this routine. }
procedure sieve;
var
n: natural;
i: integer;
multiples: naturals;
begin
{ `1` is by definition not a prime number }
primes := primes - [1];
{ find the next non-crossed number }
for n := 2 to limit do
begin
if n in primes then
begin
multiples := [];
{ We do _not_ want to remove 1 * n. }
i := 2 * n;
while i in [n..limit] do
begin
multiples := multiples + [i];
i := i + n;
end;
primes := primes - multiples;
end;
end;
end;
{ This procedures lists all numbers in `primes` }
{ and enumerates them. }
procedure list;
var
count, n: natural;
begin
count := 1;
for n := 2 to limit do
begin
if n in primes then
begin
writeLn(count:8, '.:', n:22);
count := count + 1;
end;
end;
end;
{ === MAIN program === }
begin
primes := [1..limit];
sieve;
list;
end.
sieve
任务与 list
任务分离,因此例程定义和 program
底部的主要部分都保持相当短,因此更容易理解。
program
,它从 input
读取无限数量的数值,并在最后将算术平均值打印到 output
。program arithmeticMean(input, output);
type
integerNonNegative = 0..maxInt;
var
i, sum: real;
count: integerNonNegative;
begin
sum := 0.0;
count := 0;
while not eof(input) do
begin
readLn(i);
sum := sum + i;
count := count + 1;
end;
{ count > 0: do not do division by zero. }
if count > 0 then
begin
writeLn(sum / count);
end;
end.
请注意,使用不包含负数的数据type
(这里我们将其命名为integerNonNegative
)可以减轻count
可能翻转符号的问题,这种情况被称为溢出。如果count := count + 1
变得太大,就会导致program
失败,实际上超出了范围0..maxInt
。
maxReal
,但没有编程方法可以判断sum
是否变得太大或太小,使其变得极不准确,因为无论如何任何sum
的值都可能是合法的。
time function
,它返回一个string
,以“美国”时间格式9:04 PM
。乍一看这似乎很简单,但它会变得非常具有挑战性。玩得开心!time
本身。但是,time
本身的输出没有标准化,所以我们需要自己定义所有内容。type
timePrint = string(8);
function timeAmerican(ts: timeStamp): timePrint;
const
hourMinuteSeparator = ':';
anteMeridiemAbbreviation = 'AM';
postMeridiemAbbreviation = 'PM';
type
noonRelation = (beforeNoon, afterNoon);
letterPair = string(2);
var
{ contains 'AM' and 'PM' accessible via an index }
m: array[noonRelation] of letterPair;
{ contains a leading zero accessible via a Boolean expression }
z: array[Boolean] of letterPair;
{ holds temporary result }
t: timePrint;
begin
{ fill `t` with spaces }
writeStr(t, '':t.capacity);
此回退值(在ts.timeValid
为false
的情况下)允许此function
的程序员/“用户”“盲目”地打印其返回值。输出中将会有一个明显的空白。另一个合理的“回退”值是一个空的string
。
with ts do
begin
if timeValid then
begin
m[beforeNoon] := anteMeridiemAbbreviation;
m[afterNoon] := postMeridiemAbbreviation;
z[false] := '';
z[true] := '0';
writeStr(t,
((hour + 12 * ord(hour = 0) - 12 * ord(hour > 12)) mod 13):1,
hourMinuteSeparator,
z[minute < 10], minute:1, ' ',
m[succ(beforeNoon, hour div 12)]);
这是这个问题中最复杂的部分。首先,所有传递给writeStr
的数字参数都明确地以:1
作为最小宽度规范后缀,因为有一些编译器会假设,例如,:20
作为默认值。由于我们知道timeStamp.hour
的范围是0..23
,我们可以使用div
和mod
操作,如示例所示。但是,我们需要考虑hour
值为0
的情况,通常表示为 12:00 AM(而不是零)。使用所示的Boolean
表达式和ord
进行条件“偏移” 12 可以解决这个问题。此外,这里简要提醒一下,在EP 中,succ
函数接受第二个参数。
end;
end;
timeAmerican := t;
end;
来源
- ↑ Wirth, Niklaus (1979). "The Module: a system structuring facility in high-level programming languages". proceedings of the symposium on language design and programming methodology. Berlin, Heidelberg: Springer. Abstract. doi:10.1007/3-540-09745-7_1. ISBN 978-3-540-09745-7. https://link.springer.com/content/pdf/10.1007%2F3-540-09745-7_1.pdf. Retrieved 2021-10-26.
- ↑ Cooper, Doug. "Chapter 11. The
record
Type". Oh! Pascal! (third edition ed.). p. 374. ISBN 0-393-96077-3.[…] records have two unique aspects:
First, the stored values can have different types. This makes records potentially heterogeneous—composed of values of different kinds. Arrays, in contrast, hold values of just one type, so they're said to be homogeneous.
[…]{{cite book}}
: |edition= has extra text (help); line feed character in|quote=
at position 269 (help); syntaxhighlight stripmarker in|chapter=
at position 17 (help) - ↑ Wirth, Niklaus (1973-07-00). The Programming Language Pascal (Revised Report ed.). p. 30.
Within the component statement of the with statement, the components (fields) of the record variable specified by the with clause can be denoted by their field identifier only, i.e. without preceding them with the denotation of the entire record variable.
{{cite book}}
: Check date values in:|date=
(help) - ↑ Jensen, Kathleen; Wirth, Niklaus. Pascal – user manual and report (4th revised ed.). p. 39. doi:10.1007/978-1-4612-4450-9. ISBN 978-0-387-97649-5.
The initial and final values are evaluated only once.
笔记
- ↑ 这种
record
将无法存储任何内容。在下一章中,您将学习它可能在唯一一个实例中有用。 - ↑ 实际上大多数编译器将点视为解引用指示符,并且字段名称表示从基本内存地址的静态偏移量。
- ↑ 在标准(“未扩展”)Pascal 中,ISO 标准 7185 中,
function
只能返回“简单数据类型”和“指针数据类型”的值。 - ↑ 实际上,显示的
begin … end
是多余的,因为repeat … until
本身就构成了一个框架。出于教学目的,我们建议您在通常出现语句序列的地方始终使用begin … end
。否则,您可能会将repeat … until
循环 更改为while … do
循环,忘记用适当的begin … end
框架包围循环体语句。 - ↑ 为了简便起见,省略了
packed
指示符。 - ↑ 根据大多数编译器对
maxInt
的定义。ISO 标准只要求所有在-maxInt..maxInt
范围内的算术运算都能完全正确地工作,但理论上(虽然不太可能)支持更多值。