继承的起源:一种为了性能而生的 Hack
继承的起源:一种为了性能而生的 Hack
Inheritance
(继承)是由 Simula
语言发明的,目的是为了支持 intrusive lists
(侵入式链表)、节省内存并简化 garbage collector
(垃圾回收器)。
众所周知,inheritance
是由 Simula
发明的。关于 Simula
的 History 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
中存在的一流函数的支持,而 Simula
是 ALGOL 60
的扩展。 可以预见的是,这降低了语言的表达能力:
在编写模拟程序时,我们观察到进程通常共享许多共同属性,包括数据属性和操作,但在其他方面在结构上有所不同,因此必须通过单独的声明来描述。 [...] 参数化无法提供足够的灵活性,特别是考虑到按名称调用的参数(包括过程参数)已被禁止用于进程(原因很好,请参见第 2.3.3 节)。 – Section 3.1, The Development Of The Simula Languages
由于他们的 GC
的限制,他们无法使用“参数化”(我们称之为 composition
)来自定义类。 相反,他们不得不发明 inheritance
。
支持侵入式链表
Simula
对 inheritance
的直接灵感是简化 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 月提出了“前缀”的想法。我们当时想到的是桥上的收费站,那里排着卡车或公共汽车的队列。(这个例子重新出现在
Dahl
和Nygaard
,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
是一种性能特性。 他们谈论的是代码重用和可扩展性的好处。
就代码重用和可扩展性而言,我个人更喜欢 composition
和 modules
。