我长期以来一直在想:为什么Delphi记录不能继承(因此也没有其他重要的面向对象编程特性)?
这基本上将使记录成为类的堆栈分配版本,就像C++类一样,并使“对象”(注意:不是实例)过时。我认为这没有任何问题。这也是一个很好的机会实现记录的前向声明(我仍然惊讶为什么它还缺失)。
你认为会有任何问题吗?
我长期以来一直在想:为什么Delphi记录不能继承(因此也没有其他重要的面向对象编程特性)?
这基本上将使记录成为类的堆栈分配版本,就像C++类一样,并使“对象”(注意:不是实例)过时。我认为这没有任何问题。这也是一个很好的机会实现记录的前向声明(我仍然惊讶为什么它还缺失)。
你认为会有任何问题吗?
与这个问题相关的,有两种继承方式:接口继承和实现继承。
接口继承通常意味着多态性。它意味着如果B是从A派生而来的,则类型为B的值可以存储在类型为A的位置中。对于值类型(如记录)而言,这是有问题的,因为会出现数据截取的情况。如果B比A大,那么将其存储在类型为A的位置中将截断该值——任何B在其定义中添加的字段都将丢失。
相对于这个问题,实现继承则不太有问题。如果Delphi具有记录的实现继承,但没有接口继承,那么情况就不会太糟糕。唯一的问题是将类型为A的值简单地作为类型为B的字段,即可获得实现继承的大部分功能。
另一个问题是虚方法。虚方法调度需要某种每个值标记以指示值的运行时类型,以便发现正确的重写方法。但是记录没有存储此类型的任何位置:记录的字段就是它拥有的所有字段。对象(旧版Turbo Pascal对象)可以具有虚方法,因为它们具有VMT:层次结构中定义虚方法的第一个对象隐式向对象定义的末尾添加VMT,从而使其增长。但是Turbo Pascal对象具有上述的数据截取问题,这使得它们成为了有问题的选择。对于值类型来说,虚方法实际上需要接口继承,这意味着会出现数据截取的问题。
因此,为了正确支持记录接口继承,我们需要一些解决数据截取问题的解决方案。装箱(Boxing)将是一种解决方案,但通常需要垃圾回收才能使用,并且它会在语言中引入模糊性,在这种情况下,可能不清楚您正在使用一个值还是一个引用——有点像Java中的Integer和int之类的自动装箱。至少在Java中,有针对装箱和未装箱“种类”的分别名称。另一种装箱的方式类似于Google Go中的接口,这是一种没有实现继承的接口继承,但需要单独定义接口,所有接口位置都是引用。当一个接口引用引用到值类型(如记录)时,这个值类型就被装箱了。当然,Go也有垃圾回收。
struct,record
追溯到面向对象编程进入该语言之前(Pascal时代)。
与结构体类似,记录也是内联内存块,而不是指向内存块的指针。这意味着当你将记录传递到函数中时,你传递的是一个副本,而不是指针/引用。它还意味着当你在代码中声明一个记录类型变量时,它在编译时确定其大小——在函数中使用的记录类型变量将被分配在堆栈上(不是作为指针在堆栈上,而是作为4、10、16等字节的结构)。这种固定大小与多态性不兼容。
我看到的唯一问题(可能我眼光短浅或错误)是目的。记录用于存储数据,而对象用于操作和使用该数据。为什么存储箱需要操作例程?
因为记录(records)没有虚方法表(virtual method table)。
过去,我曾使用对象(而不是类!)作为具有继承的记录。
与这里的某些人所说的不同,这样做有合理的理由。我这样做的情况涉及来自外部源(API,而不是磁盘上的任何内容-我需要完整形式的记录在内存中),第二个结构仅扩展了第一个结构。
这种情况非常罕见,虽然如此。
这与您的问题相关,涉及通过类和记录助手扩展记录和类类型的功能。根据Embarcadero关于此的文档,您可以扩展类或记录(但助手不支持操作符重载)。因此,在成员方法方面基本上可以扩展功能,但不能扩展成员数据。虽然我没有测试过,但它们支持类字段,您可以以通常的方式通过getter和setter访问它们。如果您想要接口访问添加了助手的类或记录的数据,则可能可以实现此操作(即在更改原始类或记录的成员数据时触发事件或信号)。但您无法实现数据隐藏,但它确实允许您覆盖原始类的现有成员函数。
例如:此示例适用于Delphi XE4。创建一个新的VCL表单应用程序,并使用以下代码替换Unit1中的代码:
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes,
Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, System.Types;
type
TMyArray2D = array [0..1] of single;
TMyVector2D = record
public
function Len: single;
case Integer of
0: (P: TMyArray2D);
1: (X: single;
Y: single;);
end;
TMyHelper = record helper for TMyVector2D
function Len: single;
end;
TForm1 = class(TForm)
procedure FormCreate(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
implementation
function TMyVector2D.Len: Single;
begin
Result := X + Y;
end;
function TMyHelper.Len: single;
begin
Result := Sqrt(Sqr(X) + Sqr(Y));
end;
procedure TestHelper;
var
Vec: TMyVector2D;
begin
Vec.X := 5;
Vec.Y := 6;
ShowMessage(Format('The Length of Vec is %2.4f',[Vec.Len]));
end;
procedure TForm1.Form1Create(Sender: TObject);
begin
TestHelper;
end;