继承的起源:一种为了性能而生的 Hack

Inheritance(继承)是由 Simula 语言发明的,目的是为了支持 intrusive lists(侵入式链表)、节省内存并简化 garbage collector(垃圾回收器)。

众所周知,inheritance 是由 Simula 发明的。关于 SimulaHistory of Programming Languages 会议记录告诉了我们这项发明的背后动机。让我们一起来看看。

简化垃圾回收器

Simula 创造了 inheritance 而不是使用 composition(组合),因为它可以让他们的 garbage collector 更加简单。

Simula 有一个简单的引用计数和垃圾回收实现:

由于无法找到提供足够编程灵活性的[手动内存分配]方案,我们实现了一个引用计数方案,这个想法借鉴自 Weizenbaum (1962),并且还添加了一个“最后的手段” garbage collector。 – Section 2.3.3, The Development Of The Simula Languages

使用引用计数和垃圾回收很好,但他们的 GC 有点过于简单:

一个进程可能比它的动态父进程活得更久,即包含产生该进程的生成表达式的块实例。因此,该进程可能通过其形式参数访问不存在的数据。选择的补救措施是禁止所有类型的按名称调用参数传递给进程(包括过程、标签和开关)。 – Section 2.3.3, The Development Of The Simula Languages

问题在于,可以将指向栈分配变量的指针存储在对象(在 Simula 术语中为“进程”)内部,然后从该变量的作用域返回该对象,这样存储该变量的栈帧会被释放。 这将使以后使用该对象变得不安全:“进程可能访问不存在的数据”。

使用更复杂的 garbage collector,例如当时在 Lisp 中使用的那些,这不是问题。

Simula 中简单的 GC 实现通过禁止将许多东西作为参数传递(包括将函数作为参数传递)来解决这个问题和其他问题。从某种意义上说,他们删除了 ALGOL 60 中存在的一流函数的支持,而 SimulaALGOL 60 的扩展。 可以预见的是,这降低了语言的表达能力:

在编写模拟程序时,我们观察到进程通常共享许多共同属性,包括数据属性和操作,但在其他方面在结构上有所不同,因此必须通过单独的声明来描述。 [...] 参数化无法提供足够的灵活性,特别是考虑到按名称调用的参数(包括过程参数)已被禁止用于进程(原因很好,请参见第 2.3.3 节)。 – Section 3.1, The Development Of The Simula Languages

由于他们的 GC 的限制,他们无法使用“参数化”(我们称之为 composition)来自定义类。 相反,他们不得不发明 inheritance

支持侵入式链表

Simulainheritance 的直接灵感是简化 intrusive lists 的使用,这是一种至今仍在使用的巧妙 hack,它可以使链表更有效率,但灵活性较差。

Simula 的第一个版本中,在他们发明 inheritance 和其他 OOP 功能之前,他们支持一种名为“集合”的数据结构,它是对象的任意链表(在 Simula 术语中为“进程”):

事后看来,引入“多重成员”集合是一个错误。首先,“集合”实际上是允许多次进程发生的进程序列,而简单的进程链对于大多数目的来说更为合适。[...] 由于所有进程指针都通过单独分配的“元素”对象来定向,因此进程引用中始终存在开销。 – Section 2.3.4, The Development Of The Simula Languages

与传统的链表一样,链表节点(在引文中称为“元素”)是单独分配的,从而增加了内存使用量并导致内存碎片。 相比之下,在 intrusive list 中,列表节点不是单独分配的。

Simula 的后期版本中,他们认为一种更简单的数据结构(不允许对象位于多个链表中,并且不需要低效的“元素”对象)会更好:

元素/集合概念作为列表处理的基本通用机制过于笨拙。 即使对于模拟建模,我们的经验也表明简单的进程指针可能更好,并结合进程固有的集合成员资格能力,限制为一次一个集合。 – Section 3.1, The Development Of The Simula Languages

他们决定用 intrusive lists 替换他们的传统链表,这比传统的链表更节省内存和时间。

但是,intrusive lists 要求链表节点是将在列表中的类定义的一部分。 即使 Simula 中提供了传统的组合技术,也无法满足此要求。 Simula 作者不知道他们将如何实现它,直到:

解决方案突然出现,1966 年 12 月提出了“前缀”的想法。我们当时想到的是桥上的收费站,那里排着卡车或公共汽车的队列。(这个例子重新出现在 DahlNygaard,1968 年的著作中。)一个“剥离”的列表结构,由一个“集合头”和可变数量的“链接”组成,已经被写下来了,当我们看到我们的问题都可以通过一种机制来解决,该机制将每个不同的进程(卡车、公共汽车)“粘合”到“链接”上,使每个链接-进程对成为一个块实例。 [...] 通常,一个新想法会受到相当猛烈的攻击,以测试其强度。“前缀”的想法是唯一的例外。我们立即意识到,我们现在拥有了一种全新的语言方法所必需的基础。 – Section 3.1, The Development Of The Simula Languages

Simula 中,inheritance 被称为“前缀”;基类是一个“前缀”。

因此,inheritance 的想法起源于通过让对象继承自 intrusive list 节点“link”来实现 intrusive list 的想法。 这样,intrusive lists 这种原本复杂的性能 hack 就可以很容易地被使用。

结论

出于性能原因创建功能当然没有坏处。 当然,Simula 在许多其他方面都很出色。 他们几乎发明了现代 OOP 的每个部分,而不仅仅是 inheritance; 并且他们在对象中普遍使用并发性是一个特别酷的功能,应该更多地被复制。

但有趣的是,今天没有人谈论 inheritance 是一种性能特性。 他们谈论的是代码重用和可扩展性的好处。

就代码重用和可扩展性而言,我个人更喜欢 compositionmodules