为什么Delphi记录不能继承?

发布于 2024-08-18 05:46:24 字数 181 浏览 4 评论 0原文

我长期以来一直想知道的事情:为什么 Delphi 记录不能继承(以及所有其他重要的 OOP 功能)?

这本质上会使记录成为类的堆栈分配版本,就像 C++ 类一样,并且会使“对象”(注意:不是实例)过时。我不认为它有什么问题。这也是实施记录前向声明的好机会(我仍然对为什么它仍然缺失感到困惑)。

您认为这有什么问题吗?

Something I've wondered for a long time: why aren't Delphi records able to have inheritance (and thus all other important OOP features)?

This would essentially make records the stack-allocated version of classes, just like C++ classes, and would render "objects" (note: not instances) obsolete. I don't see anything problematic with it. This would also be a good opportunity to implement forward declarations for records (which I'm still baffled as to why it's still missing).

Do you see any problems with this?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(8

遮云壑 2024-08-25 05:46:24

与这个问题相关,有两种继承:接口继承和实现继承。

接口继承通常意味着多态性。这意味着如果 B 派生自 A,则类型 B 的值可以存储在类型 A 的位置中。由于切片,这对于值类型(如记录)而不是引用类型来说是有问题的。如果 B 大于 A,则将其存储在 A 类型的位置将截断该值 - B 在其定义中添加的任何超出 A 字段的字段都将丢失。

从这个角度来看,实现继承的问题较少。如果 Delphi 有记录继承,但只是实现的记录继承,而不是接口的记录继承,那么事情还不会太糟糕。唯一的问题是,只需将 A 类型的值设置为 B 类型的字段即可完成您希望从实现继承中实现的大部分功能。

另一个问题是虚拟方法。虚拟方法分派需要某种按值标记来指示该值的运行时类型,以便可以发现正确的重写方法。但是记录没有任何地方可以存储这种类型:记录的字段是它拥有的所有字段。对象(旧的 Turbo Pascal 类型)可以具有虚拟方法,因为它们具有 VMT:层次结构中定义虚拟方法的第一个对象隐式地将 VMT 添加到对象定义的末尾,从而使其增长。但 Turbo Pascal 对象具有与上述相同的切片问题,这使它们成为问题。值类型上的虚拟方法实际上需要接口继承,这意味着切片问题。

因此,为了正确支持记录接口继承,我们需要某种解决方案来解决切片问题。装箱是一种解决方案,但它通常需要垃圾收集才能使用,并且会给语言带来歧义,可能不清楚您正在使用值还是引用 - 有点像整数与整数Java 中的 int 具有自动装箱功能。至少在 Java 中,装箱和未装箱的值类型“种类”有不同的名称。另一种装箱方式就像Google Go的接口一样,它是一种没有实现继承的接口继承,但要求接口单独定义,并且所有接口位置都是引用。当被接口引用引用时,值类型(例如记录)被装箱。当然,Go 也有垃圾收集功能。

Relevant to this question, there are two kinds of inheritance: interface inheritance and implementation inheritance.

Interface inheritance generally implies polymorphism. It means that if B is derived from A, then values of type B can be stored in locations of type A. This is problematic for value types (like records) as opposed to reference types, because of slicing. If B is bigger than A, then storing it in a location of type A will truncate the value - any fields that B added in its definition over and above those of A will be lost.

Implementation inheritance is less problematic from this perspective. If Delphi had record inheritance but only of the implementation, and not of the interface, things wouldn't be too bad. The only problem is that simply making a value of type A a field of type B does most of what you'd want out of implementation inheritance.

The other issue is virtual methods. Virtual method dispatch requires some kind of per-value tag to indicate the runtime type of the value, so that the correct overridden method can be discovered. But records don't have any place to store this type: the record's fields is all the fields it has. Objects (the old Turbo Pascal kind) can have virtual methods because they have a VMT: the first object in the hierarchy to define a virtual method implicitly adds a VMT to the end of the object definition, growing it. But Turbo Pascal objects have the same slicing issue described above, which makes them problematic. Virtual methods on value types effectively requires interface inheritance, which implies the slicing problem.

So in order to properly support record interface inheritance properly, we'd need some kind of solution to the slicing problem. Boxing would be one kind of solution, but it generally requires garbage collection to be usable, and it would introduce ambiguity into the language, where it may not be clear whether you're working with a value or a reference - a bit like Integer vs int in Java with autoboxing. At least in Java there are separate names for the boxed vs unboxed "kinds" of value types. Another way to do the boxing is like Google Go with its interfaces, which is a kind of interface inheritance without implementation inheritance, but requires the interfaces to be defined separately, and all interface locations are references. Value types (e.g. records) are boxed when referred to by an interface reference. And of course, Go also has garbage collection.

魔法少女 2024-08-25 05:46:24

在 Delphi 中,记录和类/对象是两个截然不同的东西。基本上,Delphi 记录是一个 C 结构体 - Delphi 甚至支持执行诸如拥有可以作为 4 个 16 位整数或 2 个 32 位整数访问的记录之类的语法。与struct 一样,record 的历史可以追溯到面向对象编程进入语言之前(Pascal 时代)。

与结构一样,记录也是内联内存块,而不是指向内存块的指针。这意味着当您将记录传递给函数时,您传递的是副本,而不是指针/引用。这也意味着,当您在代码中声明记录类型变量时,它会在编译时确定它有多大 - 函数中使用的记录类型变量将分配在堆栈上(不是作为堆栈上的指针,而是作为4、10、16 等字节结构)。这种固定大小不能很好地适应多态性。

Records and Classes/Objects are two very different things in Delphi. Basically a Delphi record is a C struct - Delphi even supports the syntax to do things like have a record that can be accessed as either 4 16bit integers or a 2 32bit integers. Like struct, record dates back to before object oriented programming entered the language (Pascal era).

Like a struct a record is also an inline chunk of memory, not a pointer to a chunk of memory. This means that when you pass a record into a function, you are passing a copy, not a pointer/reference. It also means that when you declare a record type variable in your code, it is determined at compile time how big it is - record type variables used in a function will be allocated on the stack (not as a pointer on the stack, but as a 4, 10, 16, etc byte structure). This fixed size does not play well with polymorphism.

从此见与不见 2024-08-25 05:46:24

我看到的唯一问题(我可能是短视或错误的)是目的。记录用于存储数据,而对象用于操作和使用所述数据。为什么储物柜需要操作程序?

The only problem I see (and I could be shortsighted or wrong) is purpose. Records are for storing data while objects are for manipulating and using said data. Why does a storage locker need manipulation routines?

路弥 2024-08-25 05:46:24

你是对的,向记录添加继承本质上会将它们变成 C++ 类。这就是你的答案:它还没有完成,因为那将是一件可怕的事情。您可以拥有堆栈分配的值类型,也可以拥有类和对象,但将两者混合是一个非常糟糕的主意。一旦这样做,您最终会遇到各种生命周期管理问题,并且最终不得不在语言中构建丑陋的黑客(例如 C++ 的 RAII 模式)来处理它们。

底线:如果您想要一个可以继承和扩展的数据类型,请使用类。这就是他们存在的目的。

编辑:为了回答云的问题,这实际上并不是可以通过一个简单的例子来证明的。整个 C++ 对象模型是一场灾难。近距离看它可能不像。你必须了解几个相互关联的问题才能真正掌握全局。 RAII 只是金字塔顶端的混乱。如果有时间的话,也许我会在本周晚些时候在我的博客上写出更详细的解释。

You're right, adding inheritance to records would essentially turn them into C++ classes. And that's your answer right there: it's not done because that would be a horrible thing to do. You can have stack-allocated value types, or you can have classes and objects, but mixing the two is a very bad idea. Once you do, you end up with all sorts of lifetime-management issues and end up having to build ugly hacks like C++'s RAII pattern into the language in order to deal with them.

Bottom line: If you want a data type that can be inherited and extended, use classes. That's what they're there for.

EDIT: In response to Cloud's question, this isn't really something that can be demonstrated via a single simple example. The entire C++ object model is a disaster. It may not look like one up close; you have to understand several interconnected problems to really grasp the big picture. RAII is just the mess at the top of the pyramid. Maybe I'll write up a more detailed explanation on my blog later this week, if I have the time.

东走西顾 2024-08-25 05:46:24

因为记录没有VMT(虚拟方法表)。

Because records don't have VMT (virtual method table).

望笑 2024-08-25 05:46:24

在过去,我使用对象(不是类!)作为具有继承的记录。

与这里有些人所说的不同,这是有正当理由的。我所做的案例涉及来自外部源的两个结构(API,不是磁盘外的任何内容——我需要内存中完整形成的记录),其中第二个结构只是扩展了第一个结构。

不过,这种情况非常罕见。

In times past I have used objects (not classes!) as records with inheritance.

Unlike what some people on here are saying there are legitimate reasons for this. The case I did it involved two structures from external sources (API, not anything off disk--I needed the fully formed record in memory), the second of which merely extended the first.

Such cases are very rare, though.

度的依靠╰つ 2024-08-25 05:46:24

您可以尝试使用 Delphi object 关键字。这些基本上是可继承的,但其行为更像记录而不是类。

请参阅此线程和此描述

You could try to use the Delphi object keyword for that. Those basically are inheritable, but behave much more like records than to classes.

See this thread and this description.

甜尕妞 2024-08-25 05:46:24

这是您问题的主题,涉及通过类和记录助手扩展记录和类类型的功能。根据 Embarcadero 的相关文档,您可以扩展类或记录(但助手不支持运算符重载)。因此基本上您可以根据成员方法扩展功能,但不能扩展成员数据)。它们支持类字段,您可以通过通常的方式通过 getter 和 setter 访问这些字段,尽管我还没有对此进行测试。如果您想通过接口访问要添加助手的类或记录的数据,您可能可以实现这一点(即,当原始类或记录的成员数据更改时触发事件或信号)。虽然您无法实现数据隐藏,但它确实允许您覆盖原始类的现有成员函数。

例如。该示例在 Delphi XE4 中运行。创建一个新的 VCL Forms 应用程序,并将 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;

请注意,结果是 7.8102,而不是 11。这表明您可以使用类或记录助手来隐藏原始类或记录的成员方法。

因此,在某种程度上,您对原始数据成员的访问就像通过更改属性而不是直接更改字段来更改声明类的单元内的值一样,以便采取适当的操作该数据的获取者和设置者。

感谢您提出问题。在寻找答案的过程中我确实学到了很多东西,这也给了我很大帮助。

布赖恩·约瑟夫·琼斯

This is on topic to your question and relates to extending the functionality of record and class types via class and record helpers. According to Embarcadero's documentation on this you can extend a class or record (but no operator overloading is supported by helpers). So basically you can extend functionality in terms of member methods but no member data). They support class fields which you could access via getters and setters in the usual way though I have not tested this. If you wanted to interface access to the data of the class or record you were adding the helper to, you could probably achieve this (ie triggering an event or signal when the member data of the original class or record was changed). You could not implement data hiding though but it does allow you to override an existing member function of the original class.

eg. This example works in Delphi XE4. Create a new VCL Forms Application and replace code from Unit1 with the following code:

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;

Notice that the result is 7.8102 rather than 11. This shows that you can hide the member methods of the original class or record with a class or record helper.

So in a way you would just treat access to the original data members just the same as you would in changing values from within the unit in which a class is declared by changing through the properties rather than the fields directly so the appropriate actions are taken by the getters and setters of that data.

Thanks for asking the question. I certainly learned a lot in trying to find the answer and it helped me out a great deal too.

Brian Joseph Johns

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文