我们今天讨论变体记录(variant record)类型。所谓变体记录类型,是指含有变体部分的记录类型。type TCoordinate = record case Kind: (Cartesian, Polar) of Cartesian: (X, Y: real); Polar: (R, Theta: real); end;
这段代码定义了一个 TCoordinate 记录类型。数学上的坐标系有两种:一是笛卡尔坐标系,一是极坐标系。通过变体记录,我们把这两种坐标系统一在了 TCoordinate 类型中。其中,field1,……,fieldn-1,fieldn 组成了记录类型 T 的共同部分,fieldn 以后的 c1,……,cm 则组成了记录类型 T 的变体部分。编译器为变体部分分配最大变体所需的内存空间,这些变体部分共享这块内存空间。fieldn 字段的值(即 c1,……,cm)决定了哪个变体有意义。文章开头所举的例子 TCoordinate 记录类型中,只有在 Kind 的值为 Cartesian 时,X、Y 才有意义;而只有在 Kind 的值为 Polar 时,R 和 Theta(θ) 才有意义。在如上定义的 TCoordinate 坐标系中,计算两点 p1 和 p2 的距离。因为 p1 和 p2 都有可能是笛卡尔坐标系(直角坐标系),也可能是极坐标系,所以,在计算两点距离时,需要区分坐标系类型,然后将极坐标系转换为直角坐标系,才能利用直角坐标系中两点之间的距离公式计算 p1 和 p2 的距离。在这段代码中,p1 采用的是笛卡尔坐标系,p2 则使用极坐标系。调用 CalculateDistance 函数计算 p1 和 p2 的距离。根据如上给出的数据,计算出 p1 和 p2 的距离为 10.46。虽然我们说,TCoordinate 中的 X 和 Y 只有在 Kind 取值 Cartesian 时才有意义,但这只是语义上的约定,编译器并没有为此设置语法上的强制力。所以,即使 p1.Kind 取值 Polar 时,你仍然可以存取 p1.X 和 p1.Y,虽然此时它们的意义是 p1.R 和 p1.Theta,但你仍然可以将其解释为 p1.X 和 p1.Y。所以,Pascal 语言的设计者 Wirth 先生警告说:在 p1.Kind 取值 Polar 时,存取 p1.X 和 p1.Y 被认为是严重的编程错误(a serious programming mistake)。虽然这种使用变体记录的方式被认为是“严重的编程错误”,但有的时候却是有意为之。如:type TFoo = record case Integer of 0: (FN: Integer); 1: (FD: Double); end;var foo: TFoo;begin foo.FD := 3.4; // 3.4 的内部表示: 400B3333 33333333 // 此时 foo.FN 为 3.4 内部表示的低 32 位 Writeln('foo.FN: ', IntToHex(foo.FN)); // 33333333end;
TCoordinate 类型定义中的 Kind 称为变体记录的类型区分符(type discriminator)或标签字段(tag field)。标签字段起到了解释变体意义的作用,但标签字段并非不可或缺,如上面的代码所示。TFoo 类型定义中,虽然有变体记录,但并没有标签字段。case ... end 的语法作用只是定义变体部分,使得 FN 和 FD 共享变体空间。此时,当 FD 有值时,FN 也有值,其意义在于程序员的解释。指针类型在 32 位程序中是 4 字节长,在 64 位程序中是 8 字节长。要想使 TFieldInfo 记录在 32 位程序中占 8 个字节,在 64 位程序中占 16 个字节,就要使得 Offset 字段在 32 位程序中占 4 个字节,在 64 位程序中占 8 个字节。但 Cardinal 类型的长度是固定的 4 字节,如何在 64 位程序中变成 8 字节?答案就是使用变体记录,而且是无标签字段的变体记录(从而不占用额外空间)。NativeUInt 类型在 32 位程序中是 4 字节长,在64 位程序中是 8 字节长。所以 _Dummy 字段正好用来为 Offset 字段占位。