5.2.1 使用记录数组
正如我前面提到的,数组表示一个重复多次的数据结构,而记录是一个具有不同元素的单一结构。鉴于这两种类型构造是正交的,所以将它们结合使用很常见(虽然可以看到数组的记录,但并不常见)。。
定义记录数组的代码与定义任何其他数组的代码一样,每个数组元素都采用特定记录类型的大小。虽然稍后我们将看到如何使用更复杂的集合或容器类(用于元素列表),但在数据管理方面,您可以通过记录数组实现很多功能。
在RecordsTest示例中,我添加了一个TMyDate类型的数组,可以通过以下代码进行分配,初始化并使用:
var
DatesList: array of TMyDate;
I: Integer;
begin
// 分配数组元素
SetLength(DatesList, 5);
// 分配随机数组元素值
for I := Low(DatesList) to High(DatesList) do
begin
DatesList[I].Year := 2000 + Random(50);
DatesList[I].Month := 1 + Random(12);
DatesList[I].Day := 1 + Random(27);
end;
// 显示数组值
for I := Low(DatesList) to High(DatesList) do
Show(I.ToString + ': ' +
MyDateToString(DatesList[I]));
由于应用使用随机数据,输出每次都会不同,但可能类似于我捕获的以下内容:
0: 2014.11.8
1: 2005.9.14
2: 2037.9.21
3: 2029.3.12
4: 2012.7.2
注解: 如果是托管记录,数组中的记录可以自动初始化,这是 Delphi 10.4 Sydney 中引入的功能,本章稍后将介绍该功能。
5.2.2 变体记录
从早期的语言版本开始,记录类型也可以有变体部分;也就是说,多个字段可以映射到同一个内存区域,即使它们的数据类型不同。(这相当于 C 语言中的联合。)另外,还可以使用这些变体字段或字段组访问记录中的同一内存位置,但要从不同的角度(数据类型方面)考虑这些值。这种类型的主要用途是存储相似但不同的数据,并获得类似于类型转换的效果(早期的语言不允许直接类型转换)。尽管有些系统库在特殊情况下会在内部使用变体记录类型,但这种类型的使用在很大程度上已被面向对象和其他现代技术所取代。
使用变体记录类型不是类型安全的,因此不推荐使用,尤其是对于初学者。因此,我决定不向你展示实际示例,也不详细介绍这一功能。如果你真的需要提示,可以看看我在 "可变类型开放式数组形参 "一节的演示中对 TvarRec
的使用。
5.2.3 字段对齐
与记录相关的另一个高级主题是它们的字段对齐方式,这也有助于了解记录的实际大小。如果您查看程序库,通常会看到在记录中packed
关键字:这意味着记录应该使用尽可能少的字节,即使这会导致更慢的数据访问操作。
传统上,这种差异通常与各个字段的16位或32位对齐方式有关,因此即使只使用8位,一个字节后面跟一个整数可能最终占据32位。这是因为在32位边界上访问后面的整数值使代码执行得更快。
注解: 字段的大小和对齐取决于类型的大小。对于任何大小不是2的幂(或2^N)的类型,大小是下一个较高的2的幂。例如,Extended类型使用10个字节,在记录中占用16个字节(除非记录是紧凑的)。
一般来说,数据结构(如记录)会使用字段对齐来提高某些 CPU 架构对单个字段的访问速度。
你可以使用不同的参数来改变 KaTeX parse error: Expected '}', got 'EOF' at end of input: …LIGN 编译器指令。使用 {ALIGN 1} 时,编译器将通过使用所有可能的字节来节省内存使用量,就像为记录使用打包规范一样。在另一个极端,{$ALIGN 16} 将使用最大对齐方式。其他选项则使用 4 和 8 对齐方式。
举例来说,如果我回到 RecordsTest 项目,并在记录定义中添加关键字 packed:
type
TMyDate = packed record
Year: Integer;
Month: Byte;
Day: Byte;
end;
对SizeOf
调用的输出现在将返回6而不是8。
作为一个更高级的示例(如果你还不是一个流利的 Object Pascal 开发人员,可以跳过这个示例),让我们来看看下面的结构(可在 AlignTest 示例中找到):
type
TMyRecord = record
C: Byte;
W: Word;
B: Boolean;
I: Integer;
D: Double;
end;
使用{$ALIGN 1}
,结构占用16个字节(由SizeOf
返回的值),字段的相对内存地址如下:
C: 0; W: 1; B: 3; I: 4; D: 8
注意: 相对地址的计算方法是:分配记录并计算结构指针的数值与给定字段指针的数值之差,表达式如下 UIntPtr(@MyRec.w) - UintPtr(@MyRec1)。本章稍后将介绍指针和地址 (@) 操作符的概念。
相比之下,如果将对齐方式改为 4(可优化数据访问) 大小将为 20 字节,相对地址为:
C: 0; W: 2; B: 4; I: 8; D: 12
如果选择极端选项使用{$ALIGN 16}
,结构需要24字节,并将字段映射如下:
C: 0; W: 2; B: 4; I: 8; D: 16
5.2.4 对于With语句呢?
在处理记录或类时使用的另一个传统语言语句是 with 语句。这个关键字曾经是 Pascal 语法所特有的,但后来被引入到 JavaScript 和 Visual Basic 中。这个关键字可以非常方便地减少代码的编写,但也可能变得非常危险,因为它使代码的可读性大大降低。
围绕 with 语句有很多争论,我倾向于同意应尽量少用。无论如何,我觉得还是有必要将其纳入本书(与 goto 语句不同)。
注解: 关于从 Object Pascal 语言中移除 goto 语句是否有意义,还有人讨论过是否要从该语言的移动版本中移除 with。虽然有一些合理的用法,但考虑到 with 语句可能导致的范围界定问题,我们有充分的理由停止使用这一功能(或者像 C# 那样将其改为需要别名)。
with语句只不过是一种简写。当你需要引用记录类型变量(或对象)时,你可以使用with语句,而不是每次都重复其名称。例如,在介绍记录类型时,我写了这段代码:
var
ABirthday: TMyDate;
begin
ABirthday.Year := 2008;
ABirthday.Month := 2;
ABirthday.Day := 14;
使用with语句,我可以修改此代码的最后一部分,如下所示:
with ABirthday do
begin
Year := 2008;
Month := 2;
Day := 14;
end;
在 Object Pascal 程序中,可以使用这种方法来引用组件和其他类。当您使用组件或一般类时,with语句允许您跳过编写一些代码,特别是对于嵌套的数据结构。
那么,为什么我不鼓励使用 with 语句呢?原因是它可能导致难以捕捉的细微错误,比如掩盖另一个变量。
虽然有些难以发现的错误不容易在本书中解释清楚,让我们考虑一个可能引起困惑的温和场景。这是一个记录类型和使用记录的一些代码:
type
TMyRecord = record
MyName: string;
MyValue: Integer;
end;
procedure TForm1.Button2Click(Sender: TObject);
var
Record1: TMyRecord;
begin
with Record1 do
begin
MyName := 'Joe';
MyValue := 22;
end;
with Record1 do
Show(Name + ': ' + MyValue.ToString);
end;
程序对吗?应用程序编译和运行,但其输出可能不是您所期望的(至少乍一看不是):
Form1: 22
输出的字符串部分不是之前设置的记录值。原因是第二个with语句错误地使用了Name字段,这不是记录字段,而是在作用域内的另一个字段(具体是Button2Click方法所属的窗体对象的名称)。
如果您写成:
Show(Record1.Name + ': ' + Record1.MyValue.ToString);
编译器会显示一个错误消息,指示给定的记录结构没有Name字段。
总的来说,我们可以说,由于 with 语句在当前作用域中引入了新的标识符,我们可能会隐藏已有的标识符,或错误地访问同一作用域中的另一个标识符。这是不鼓励使用 with 语句的一个很好的理由。此外,还应避免使用多个 with 语句,例如:
with MyRecord1, MyDate1 do...
接下来的代码可能会非常难读,因为对于块中使用的每个字段,你都需要考虑它指的是哪条记录。