上次我们简单介绍了一下 Delphi 的记录类型,并说明记录类型的变量具有“赋值即拷贝”的特点,即将一个记录变量 A 赋值给另一个记录变量 B 时,编译器会将 A 的所有字段拷贝给 B 的相应字段。今天我们再来探讨一下这个问题。其实所有基本类型都是“赋值即拷贝”,譬如整型、浮点型、枚举型、字符型、布尔型等等,即使像 ShortString、set、静态数组这些非基本类型,也是“赋值即拷贝”。这些类型的特点都是比较简单,长度有限,所以“赋值即拷贝”的效率比较高。对于长字符串 string、动态数组等非基本类型,因为数据量大,长度不可预测,所以实行的都是“赋值即引用”,这些我们在讨论字符串和数组的文章中都说过了,此处不再赘述。在这个例子中,我们定义了一个 TPerson 记录类型,包含四个字段,其中的 Children 字段又是基类型为 TPerson 的动态数组类型。TPerson 是一个比较典型的记录类型,其中的字段既有整型、字符型这样简单的基本类型,又有字符串、动态数组这样复杂的非基本类型。在测试程序中,我们声明了两个 TPerson 类型的变量 david 和 john。在代码部分,首先对 david 记录进行初始化,然后构造 david.Children 的动态数组,从 SetLength 的参数,我们知道这个动态数组的形态为 array[0..1] of TPerson。接下来是对这个数组的赋值。注意,这里我们使用了 with 结构。对于 Pascal 中的 with 结构,向来就有争论。我们认为,with 结构就如指针一样,滥用自然会导致问题,但谨慎使用,还是有益的。下面就是记录的赋值,第39行将 david 记录赋值给 john 变量。我们在上文已经讨论过 Delphi 是如何实现简单记录的赋值的,今天的记录类型比较复杂,那么 Delphi 又是如何实现记录赋值的呢?在讨论 _CopyRecord 之前,我们先看一下 TPerson 记录在内存中的存储方式,如下图:在 david 记录中,Name 字段实际是一个字符指针(图中以 NamePtr 表示),指向 'David' 这个字符串的内存地址。关于字符串的内存结构,我们以前也讨论过,请参看有关 string 的文章,此处不再赘述。Children 字段是一个动态数组,david 记录中的这个字段实际也是一个指针,指向数组在内存中的位置。Children 数组元素的存储方式与 david 记录一样,只不过它们的 Children 字段都是 nil 值。Delphi 的记录类型与动态数组类型一样,除了数据本身之外,编译器还维护着一个运行时的类型记录,其定义如下(摘自 System.pas):对于记录类型,其 TypeInfo 记录包含三个字段:- Name 字段,其值为用 type 定义的记录类型,在我们这个例子中是 'TPerson' 字符串。
- TFieldTable 字段,这是一个记录类型,Size 字段表示 TPerson 记录的大小,Count 为 TPerson 记录类型的字段数,然后是一个数组,其基类型是 TFieldInfo,其中记录了字段的类型和在整个记录中的偏移量(Offset)。
有了这些准备,我们就可以讨论 _CopyRecord 过程了,见下图:_CopyRecord 过程很长,我们这里做了一些简化,去掉了一些与今天的讨论没有多大关系的内容。由图可见,_CopyRecord 需要三个参数:一个目标记录变量(Dest),一个源记录变量(Source),这两个参数都是无类型参数。第三个参数就是编译器所维护的记录类型的运行时类型信息,这是一个指向 TTypeInfo 记录的指针。过程的实现部分,首先是获取指向 TFieldTable 记录的指针,并赋给 FT。接下来就是一个 For 循环,分别检视记录的每一个字段,并根据该字段的类型调用不同的拷贝程序,譬如,对于 UnicodeString,调用 _UStrAsg 过程拷贝字符串,而对于动态数组,则调用 _DynArrayAsg 拷贝动态数组,并相应更新字段的偏移量 Offset。对于不需要特定拷贝程序的字段,譬如整型、字符型等等这些基本类型,则直接调用 Move 拷贝字段内容。从这个 _CopyRecord 实现过程的简单叙述可以看出,记录的赋值/拷贝的成本是很高的,特别对于复杂记录类型,更是如此。明白了这一点,我们在具体的编程过程中,就要尽量少进行记录赋值,特别要注意的是过程/函数的参数。如本例中的 PrintPersonInfo 过程的参数 P,因为 P 是 TPerson 这样的复杂类型,如果过程/函数中需要对源记录进行修改,则要将 P 声明为:procedure PrintPersonInfo(var P: TPerson);
procedure PrintPersonInfo(const P: TPerson);
如此声明之后,在实际调用 PrintPersonInfo 过程时,将会传递记录(实际参数)的地址。否则,如果这样声明:procedure PrintPersonInfo(P: TPerson);
在实际调用 PrintPersonInfo 过程时,编译器会生成复制整个 TPerson 记录给 P 的指令!