【问题标题】:Why can't Delphi records have inheritance?为什么 Delphi 记录不能有继承?
【发布时间】:2011-01-06 22:12:27
【问题描述】:

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

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

你觉得这有什么问题吗?

【问题讨论】:

  • 当您说 protoyping 时,我认为您真正的意思是 前向声明
  • 是的,抱歉,一时失忆。 :P 这基本上是一样的,只是在 Delphi 和 C++ 世界之间拼写不同。虽然我更喜欢 Delphi 这个名字,但更不言自明。
  • 其实prototype也只用于C++中的函数。 C++ 对类类型使用前向声明,就像 Delphi 一样。
  • 它们对我来说仍然意味着同样的事情,只是他们决定在以后的 C++ 中使用另一个名称。还是有我不知道的更深刻的区别?
  • 你可以看看 JavaScript 或类似的语言,它没有对象继承和类,而是使用对象原型。因此,一般而言,“原型”一词具有多种含义,并且最好避免使用“语言 X 缺乏原型”之类的问题。因为在这样的问题中,任何类型的原型都适合。

标签: delphi inheritance class records


【解决方案1】:

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

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

从这个角度来看,实现继承的问题较少。如果 Delphi 有记录继承,但只有实现,而不是接口,事情不会太糟糕。唯一的问题是,简单地将 A 类型的值设置为 B 类型的字段可以完成您希望实现继承之外的大部分工作。

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

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

【讨论】:

  • 我现在明白了,所以基本上问题主要在于切片。但是,由于每个变量实际上都是对实际对象的引用,装箱实际上不会像类一样制作记录吗?或者该对象会以某种方式实际上是堆栈分配的?顺便说一句,很荣幸你回答我的问题。让一个著名的开发者回答它,让我的脸上露出笑容。非常感谢! :)
  • 盒装记录会,嗯,通过引用来引用;存储值的位置,无论是在堆上还是在堆栈上,都是依赖于例如的实现细节。逃逸分析(堆栈位置是否比引用长)。但它们不一定就像类一样;如果盒装与非盒装类型具有不同的名称或语法,或者像 Go 并使用仅接口继承,那么可以设想盒装类型的多态性(当被引用引用时)但不能直接用于位置(所以避免切片)。有点像Java的int/Integer,IOW。
【解决方案2】:

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

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

【讨论】:

  • 我知道记录相当于结构,但仍然......这并不一定排除扩展它们。我不太确定,但我认为也可以有变体类,所以变体记录作为类不会造成太大问题。我知道记录是堆栈分配的,而不是堆分配的类,但是 C++ 类也是堆栈分配的,所以我在这里看不到什么问题。
  • 固定大小适用于多态性。 Turbo Pascal 和 C++ 都有固定大小的值类型类,它们仍然支持继承和虚函数。
  • @Rob Kennedy:您的意思是“对象”类型?它们仍然存在于 Delphi 中,只是它们没有运算符重载、虚函数(我不确定)并且在属性或在它们之后引入的任何东西方面存在一些问题。
  • 对。它们具有虚函数,但它们不支持编译器管理的类型,如其字段的接口或字符串。它们在 Delphi 中从根本上被破坏了,所以我不认为它们以任何有意义的方式存在于 Delphi 中。但是它们在 Turbo Pascal 中的存在证明了支持继承的值类型是可能的。 (向记录添加虚拟方法会比较棘手,但虚拟方法不是继承的基本属性。)
  • C++ 类不只是改变了默认可见性子句的结构吗?至少 AFAIR 是早期 C++ 版本的情况
【解决方案3】:

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

【讨论】:

  • 是的,但是 Delphi 已经有了“带有方法的记录”,所以基本上它们现在也可以操作数据了。为什么不一路走?无论哪种方式,为什么不使用堆栈分配的对象,无论它们的名称是什么?这样一来,人们就不会再抱怨缺乏垃圾收集了,一切都会变得美好。
  • 在Delphi中可以添加到记录的方法只是静态方法的语法糖,就像C#中的扩展方法一样。没有多态空间,因为没有使用虚拟方法表。
  • @Cloud737:想象一下堆栈上的一个对象,它使用 RAII 来管理临界区或其他同步对象。那太好了,因为为此目的使用接口(就像我现在所做的那样)会引入太多开销。但是,要使其有用,您需要能够准确指定何时创建和销毁它。在 C++ 中可能,但在当前规则下的 Delphi 中则不然。
  • Delphi 已经有了继承、VMT 和 DMT 的“记录”。它们由旧的“object”关键字声明。但我不建议人们使用旧的基于堆栈的对象,因为如果您尝试使用 TurboPascal 7.0 之后引入的代码构造,编译器会有一些代码生成错误,例如属性。
  • @Cloud737:解引用不是最大的问题(通常你不会调用任何方法,把工作留给构造函数和析构函数),内存管理器才是。创建基于堆栈的对象只涉及操作 CPU 寄存器以(取消)分配内存。在单线程程序中访问内存管理器的成本更高,在多线程程序中它可能是一个杀手。至于丑陋的问题,它们会是什么?
【解决方案4】:

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

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

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

【讨论】:

  • 我对您所指的 C++ 问题不太熟悉。我没有过多地关注RAII。您能否发布一个示例说明为什么会出现问题?
  • -1 错误地认为 RAII 是一种必要但丑陋的 hack。
  • 这是一个误解,mghie? RAII 是我见过的最糟糕的语言特性之一,在任何编程语言中,我坦率地对这么多 C++ 编码人员似乎认为它是一件好事感到困惑。
  • 接口继承与值类型,即使不同大小的值类型成为彼此的子类型成为可能,因此切片出现在图片中,这是一个非常糟糕的主意。引入 vtable,情况会变得更糟:你可能会得到一个对于它的 vtable 引用的某些方法来说太小的类型,并且你可能会遇到覆盖堆栈的缓冲区溢出问题。具有值类型的接口继承:说不。
  • @Mason:“D 的案例”(ddj.com/hpc-high-performance-computing/217801225)以及“D 编程语言”的概述和目录页面(my.safaribooksonline.com/9780321659538
【解决方案5】:

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

【讨论】:

    【解决方案6】:

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

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

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

    【讨论】:

    • 这些情况确实很常见,例如当使用像标题 + 不定长度数据区这样的对象时。
    【解决方案7】:

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

    看到这个thread和这个description

    【讨论】:

    • 啊!不!不要使用 object 关键字!它已被弃用多年,从 D2010 开始,甚至根本没有得到真正的正确支持。
    【解决方案8】:

    这是您的问题的主题,涉及通过类和记录助手扩展记录和类类型的功能。根据 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。这表明您可以使用类或记录助手隐藏原始类或记录的成员方法。

    因此,在某种程度上,您只需处理对原始数据成员的访问,就像您在声明类的单元中更改值一样,通过更改属性而不是直接更改字段,以便适当的操作由该数据的 getter 和 setter 获取。

    感谢您提出问题。在尝试寻找答案的过程中,我当然学到了很多东西,这对我也有很大帮助。

    布赖恩·约瑟夫·约翰斯

    【讨论】:

      猜你喜欢
      • 2018-05-11
      • 2010-09-28
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-10-20
      相关资源
      最近更新 更多