面向对象范式的历史
引言:面向对象,聚讼纷纷
面向对象编程(Object-Oriented Programming, OOP)是计算机科学史上最具影响力、也最具争议性的编程范式之一。从 1960 年代 Simula 语言的诞生,到今天几乎所有主流语言都或多或少地支持面向对象特性,OOP 走过了超过半个世纪的演化之路。
然而,"面向对象"这个概念本身从未有过一个被所有人接受的统一定义。Alan Kay 认为 OOP 的核心是消息传递(messaging);Bjarne Stroustrup 强调的是数据抽象与类型层次;Luca Cardelli 和 Peter Wegner 在其经典论文中将 OOP 定义为封装、继承与多态的组合(Cardelli & Wegner, 1985)。不同的语言设计者对 OOP 有着截然不同的理解,这些理解的差异直接塑造了各自语言的面貌。
本文试图以时间为轴,以语言为线索,梳理面向对象范式从萌芽到成熟、从正统到变异的完整历史。我们将看到:每一门语言在设计 OOP 特性时,都在表达力、性能、安全性与简洁性之间做出了不同的取舍,而这些取舍本身就构成了编程语言设计思想的演进史。
OOP 语言发展时间线
timeline
title 面向对象编程语言发展时间线
section 萌芽期 (1960s)
1967 : Simula 67<br>第一门OOP语言<br>类/对象/继承
section 探索期 (1970s-1980s)
1980 : Smalltalk-80<br>纯消息传递<br>一切皆对象
1984 : Objective-C<br>动态消息传递<br>分类扩展
1986 : Eiffel<br>契约式设计
1987 : Self<br>原型继承<br>JIT编译先驱
1988 : CLOS<br>多方法<br>元对象协议
section 工业化 (1990s)
1991 : Python<br>多范式<br>鸭子类型
1995 : Java<br>JVM + GC<br>单继承+接口
1995 : Ruby<br>Mixin<br>元编程
1995 : JavaScript<br>原型链<br>Web普及
section 现代化 (2000s-)
2000 : C#<br>属性/LINQ<br>泛型具化
2004 : Scala<br> Trait<br>OOP+FP统一
2009 : Go<br>组合优于继承<br>隐式接口
2012 : TypeScript<br>渐进类型<br>结构化类型
2015 : Rust<br>所有权系统<br>Trait组合
OOP 语言特性对比矩阵
graph TB
subgraph 静态类型
A[Simula 67<br>基于类/单继承]
B[C++<br>基于类/多重继承]
C[Eiffel<br>基于类/多重继承<br>契约式设计]
D[Java<br>基于类/单继承+接口<br>GC]
E[C#<br>基于类/单继承+接口<br>泛型具化]
F[Scala<br>基于类/Trait<br>OOP+FP统一]
G[Go<br>无继承/隐式接口<br>组合]
H[Rust<br>Trait组合<br>所有权系统]
end
subgraph 动态类型
I[Smalltalk-80<br>基于类/单继承<br>纯消息传递]
J[Self<br>基于原型/委托]
K[Ruby<br>基于类/Mixin<br>开放类]
L[JavaScript<br>基于原型/原型链]
M[Python<br>基于类/多重继承<br>鸭子类型]
end
subgraph 混合类型
N[Objective-C<br>基于类/单继承<br>动态消息]
O[TypeScript<br>基于类/结构化类型<br>渐进类型]
end
style A fill:#e1f5ff
style B fill:#ffe1e1
style C fill:#e1ffe1
style D fill:#fff5e1
style E fill:#f5e1ff
style F fill:#e1f5ff
style G fill:#ffe1f5
style H fill:#e1ffe1
style I fill:#f5e1e1
style J fill:#e1f5ff
style K fill:#fff5e1
style L fill:#ffe1e1
style M fill:#f5e1ff
style N fill:#e1ffe1
style O fill:#fff5e1
OOP 语言影响关系图
graph LR
subgraph 基础语言
ALGOL60[ALGOL 60<br>1960]
end
subgraph 第一代OOP
SIM[Simula 67<br>1967]
end
subgraph 理论探索
SMALL[Smalltalk-80<br>1980]
SELF[Self<br>1987]
end
subgraph 工业化先驱
CPP[C++<br>1985]
OBJC[Objective-C<br>1984]
EIFFEL[Eiffel<br>1986]
end
subgraph Web时代
JS[JavaScript<br>1995]
JAVA[Java<br>1995]
RUBY[Ruby<br>1995]
PYTHON[Python<br>1991]
end
subgraph 现代语言
CSHARP[C#<br>2000]
SCALA[Scala<br>2004]
GO[Go<br>2009]
RUST[Rust<br>2015]
TS[TypeScript<br>2012]
end
ALGOL60 -->|块结构/作用域| SIM
ALGOL60 -->|语法基础| CPP
ALGOL60 -->|语法基础| OBJC
SIM -->|类/对象/继承| SMALL
SIM -->|类/对象/继承| CPP
SIM -->|类/对象/继承| EIFFEL
SIM -->|类/对象/继承| JAVA
SMALL -->|消息传递| OBJC
SMALL -->|一切皆对象| RUBY
SMALL -->|动态类型| PYTHON
SELF -->|原型继承| JS
SELF -->|JIT技术| JAVA
CPP -->|语法风格| JAVA
CPP -->|语法风格| CSHARP
OBJC -->|分类| CSHARP
OBJC -->|协议| SWIFT[Swift]
JAVA -->|JVM生态| SCALA
JAVA -->|简化思路| GO
JS -->|语法基础| TS
GO -->|组合思想| RUST
style ALGOL60 fill:#e0e0e0
style SIM fill:#ffcccc
style SMALL fill:#ccffcc
style SELF fill:#ccccff
style CPP fill:#ffccff
style OBJC fill:#ffffcc
style EIFFEL fill:#ccffff
style JS fill:#ffe0b2
style JAVA fill:#b2dfdb
style RUBY fill:#f8bbd9
style PYTHON fill:#c5cae9
style CSHARP fill:#d1c4e9
style SCALA fill:#b2ebf2
style GO fill:#c8e6c9
style RUST fill:#ffccbc
style TS fill:#fff9c4
style SWIFT fill:#e1bee7
前 OOP 时代:过程式编程的局限(1950s–1960s)
FORTRAN 与 ALGOL:结构化的起点
在面向对象出现之前,编程世界由过程式编程(procedural programming)主导。1957 年的 FORTRAN 和 1958 年的 ALGOL 58(后演化为 ALGOL 60)确立了子程序(subroutine)、块结构(block structure)和词法作用域(lexical scoping)等基本概念。
ALGOL 60 尤其重要。它由 Peter Naur 等人设计,引入了 BNF 范式来描述语法,确立了结构化编程的基础。ALGOL 60 的块结构和嵌套作用域直接影响了后来几乎所有编程语言的设计(Naur et al., 1963)。
然而,过程式编程有一个根本性的问题:数据与操作数据的过程是分离的。随着程序规模的增长,维护数据与过程之间的一致性变得越来越困难。程序员需要一种方式将相关的数据和操作绑定在一起——这就是面向对象思想的萌芽。
ALGOL 的遗产
值得特别指出的是,ALGOL 家族对 OOP 的影响远不止于提供语法基础。ALGOL 60 的按名调用(call by name)机制启发了后来的惰性求值思想;ALGOL 68 则引入了正交设计(orthogonal design)原则,即语言中的基本概念应当可以自由组合而不产生意外限制。这一原则后来成为评价语言设计优劣的重要标准。
ALGOL W(1966)由 Niklaus Wirth 和 C. A. R. Hoare 设计,引入了记录类型(record type)和引用类型(reference type),这可以被视为"将数据组织在一起"的早期尝试——虽然还不是真正的对象,但已经有了数据封装的雏形。
Simula:面向对象的创世纪(1962–1967)
从模拟到编程范式
面向对象编程的历史始于挪威。1962 年,Ole-Johan Dahl 和 Kristen Nygaard 在挪威计算中心(Norwegian Computing Center)开始设计 Simula I,最初目的是为离散事件模拟(discrete event simulation)提供一种专用语言。
Simula I 基于 ALGOL 60 构建,增加了进程(process)的概念——每个模拟实体都是一个拥有自己状态和行为的进程。这个设计决策看似自然,却蕴含着深刻的洞见:现实世界中的实体可以被建模为拥有状态和行为的自治单元。
1967 年,Dahl 和 Nygaard 发布了 Simula 67,这是真正意义上的第一门面向对象编程语言。Simula 67 引入了以下关键概念(Nygaard & Dahl, 1978):
- 类(Class):将数据和操作数据的过程封装在一起的模板
- 对象(Object):类的实例,拥有自己的状态
- 继承(Inheritance):通过
SUBCLASS关键字实现类的层次化组织 - 虚方法(Virtual Procedure):允许子类覆盖父类的方法,实现运行时多态
- 协程(Coroutine):支持协作式多任务,这是 Simula 模拟背景的遗产
Simula 的设计取舍
Simula 67 做出了几个影响深远的设计决策:
选择基于类的对象系统:Simula 选择了"先定义类,再创建对象"的模式,而非"直接创建对象再复制"的原型模式。这个选择源于 Simula 的模拟背景——在模拟中,你需要先定义实体的类型(如"顾客"、“服务台”),然后创建具体的实例。这一选择奠定了后来 C++、Java 等主流 OOP 语言的基调。
选择单继承:Simula 只支持单继承,即一个类只能有一个直接父类。这简化了实现,避免了后来 C++ 多重继承带来的菱形继承(diamond problem)等复杂问题。
静态类型系统:作为 ALGOL 的后代,Simula 保留了静态类型检查。对象的类型在编译时确定,这提供了类型安全性,但也限制了灵活性。
值得注意的是,Simula 的设计者们最初并没有意识到他们创造了一种新的编程范式。Nygaard 后来回忆说,他们只是在解决模拟领域的具体问题,"面向对象"这个术语是后来才被赋予的(Nygaard & Dahl, 1978)。
Smalltalk:纯粹的面向对象哲学(1972–1980)
Alan Kay 的愿景
如果说 Simula 是面向对象的发明者,那么 Smalltalk 就是面向对象的布道者。Smalltalk 由 Alan Kay 在 Xerox PARC(帕洛阿尔托研究中心)领导开发,从 1972 年的 Smalltalk-72 到 1980 年的 Smalltalk-80,经历了多次重大演化。
Alan Kay 的 OOP 愿景与 Simula 有着根本性的不同。Kay 后来明确表示(Kay, 1993):
“I invented the term ‘object-oriented’, and I can tell you I did not have C++ in mind.”
(我发明了"面向对象"这个术语,我可以告诉你,我脑子里想的不是 C++。)
Kay 认为 OOP 的核心不是类和继承,而是消息传递(message passing)。在他的构想中,对象就像生物细胞或网络上的计算机——它们是自治的实体,通过发送和接收消息来协作。对象内部如何处理消息是完全封装的,外部不需要也不应该知道。
Smalltalk 的革命性设计
Smalltalk-80 实现了一系列激进的设计选择(Goldberg & Robson, 1983):
一切皆对象:在 Smalltalk 中,整数、布尔值、字符、甚至类本身都是对象。数字 3 是 SmallInteger 类的一个实例,true 是 True 类的唯一实例。这种彻底的一致性意味着 3 + 4 实际上是向对象 3 发送消息 +,参数为 4。
纯消息传递:所有的计算都通过消息传递完成。没有运算符、没有控制结构——if-else 是向布尔对象发送 ifTrue:ifFalse: 消息,while 循环是向块(Block)对象发送 whileTrue: 消息。
动态类型:Smalltalk 是动态类型语言,变量没有类型声明,任何变量可以引用任何对象。类型检查在运行时进行——如果一个对象不理解收到的消息,它会抛出 doesNotUnderstand: 异常。
元类系统:每个类本身也是一个对象,是其元类(metaclass)的实例。元类定义了类的行为(如创建实例的方法)。这种"类也是对象"的设计实现了完美的概念一致性。
反射(Reflection):Smalltalk 支持强大的反射能力——程序可以在运行时检查和修改自身的结构。这使得 Smalltalk 成为一个极其灵活的环境,也为后来的元编程(metaprogramming)奠定了基础。
集成开发环境:Smalltalk 不仅是一门语言,更是一个完整的计算环境。它包含了图形用户界面、代码浏览器、调试器——这是世界上第一个现代意义上的 IDE。
Smalltalk 的取舍与代价
Smalltalk 的纯粹性带来了优雅,但也付出了代价:
- 性能:一切皆对象和纯消息传递意味着即使是简单的整数运算也需要消息分派(message dispatch),这在 1980 年代的硬件上造成了显著的性能开销。
- 静态分析困难:动态类型使得编译器几乎无法进行静态优化,也使得大型项目的维护变得困难——你无法在编译时发现类型错误。
- 封闭生态:Smalltalk 的"一切都在虚拟机镜像(image)中"的模式使得它难以与外部系统集成。
尽管如此,Smalltalk 对后世的影响是巨大的。它证明了 OOP 可以是一种完整的计算模型,而不仅仅是在过程式语言上添加的语法糖。
Self:原型继承的先驱(1986–1991)
抛弃类的实验
在 Smalltalk 的影响下,David Ungar 和 Randall B. Smith 在斯坦福大学(后转至 Sun Microsystems)设计了 Self 语言(Ungar & Smith, 1987)。Self 提出了一个激进的问题:如果消息传递才是 OOP 的核心,那么我们真的需要类吗?
Self 的回答是:不需要。Self 引入了基于原型的面向对象(prototype-based OOP):
- 没有类:对象直接从其他对象克隆(clone)而来
- 原型委托:对象通过父链接(parent link)将无法处理的消息委托给父对象,这取代了类继承
- 槽(Slot):对象的状态和行为统一存储在"槽"中——数据槽存储值,方法槽存储代码,两者没有本质区别
Self 的深远影响
Self 本身从未成为主流语言,但它的影响极其深远:
- JavaScript 的原型系统直接源自 Self(Brendan Eich 明确承认了这一点)
- Lua 的元表(metatable)机制也受到了 Self 的启发(Ierusalimschy et al., 2007)
- Self 团队开发的自适应优化(adaptive optimization)和 JIT 编译技术后来被应用于 Java HotSpot 虚拟机——Self 的 VM 工程师 Lars Bak 后来领导了 HotSpot 和 Google V8 引擎的开发
C++:实用主义的面向对象(1979–1998)
Bjarne Stroustrup 的设计哲学
1979 年,Bjarne Stroustrup 在贝尔实验室开始设计 C with Classes,后于 1983 年更名为 C++。Stroustrup 的出发点非常明确:他在剑桥大学攻读博士时使用过 Simula,欣赏其组织大型程序的能力,但无法忍受其运行时性能。他希望将 Simula 的抽象能力与 C 的效率结合起来(Stroustrup, 1993)。
这一设计哲学可以概括为 Stroustrup 的名言:
“C++ is designed to allow you to express ideas, but if you don’t want to use a feature, you shouldn’t have to pay for it.”
(C++ 的设计允许你表达想法,但如果你不想使用某个特性,你不应该为它付出代价。)
这就是著名的零开销抽象(zero-overhead abstraction)原则。
C++ 的 OOP 特性演化
C++ 的 OOP 特性经历了漫长的演化:
C with Classes(1979–1983):
- 类与数据封装(
public/private) - 派生类(继承)
- 构造函数与析构函数
- 内联函数
C++ 1.0(1985):
- 虚函数(virtual function)实现运行时多态
- 运算符重载
- 引用类型
C++ 2.0(1989):
- 多重继承(multiple inheritance)——这是一个极具争议的决定
- 抽象类(纯虚函数)
- 静态成员函数
C++ 标准化(1998, C++98):
- 模板(template)——虽然不是 OOP 特性,但模板元编程后来成为 C++ 的重要范式
- 标准模板库(STL)
- 异常处理
- RTTI(运行时类型信息)
C++ 的关键设计取舍
多重继承 vs 单继承:C++ 选择支持多重继承,这带来了菱形继承问题(diamond problem)——当一个类通过两条路径继承同一个基类时,基类的成员会出现歧义。C++ 通过虚继承(virtual inheritance)来解决这个问题,但这增加了语言的复杂性和实现的开销。后来的 Java 和 C# 都选择了单继承 + 接口的方案来规避这个问题。
值语义 vs 引用语义:与 Smalltalk 和后来的 Java 不同,C++ 的对象默认具有值语义——对象可以在栈上分配,赋值操作默认是复制。这提供了更好的性能和确定性的资源管理(通过 RAII 模式),但也使得对象的行为更加复杂(需要处理拷贝构造函数、赋值运算符等)。
编译时多态 vs 运行时多态:C++ 同时支持通过虚函数实现的运行时多态和通过模板实现的编译时多态。模板在编译时展开,没有运行时开销,但会导致代码膨胀和编译时间增长。这种双轨制体现了 C++ 的实用主义:让程序员根据具体场景选择最合适的机制。
手动内存管理:C++ 没有垃圾回收器,对象的生命周期由程序员显式管理。这是对性能的极致追求,但也是 bug 的温床。C++ 后来通过智能指针(unique_ptr、shared_ptr)和 RAII 模式来缓解这个问题,但从未完全消除手动管理的复杂性。
C++ 对 OOP 理论的贡献
C++ 的实践推动了 OOP 理论的发展。Gamma、Helm、Johnson 和 Vlissides 的经典著作《设计模式》(Gamma et al., 1994)主要基于 C++ 和 Smalltalk 的经验,提出了 23 种设计模式,其核心原则——面向接口编程而非面向实现编程(program to an interface, not an implementation)和优先使用组合而非继承(favor composition over inheritance)——深刻影响了后来所有 OOP 语言的最佳实践。
Objective-C:消息传递的 C 扩展(1984)
Brad Cox 的软件 IC 愿景
1984 年,Brad Cox 和 Tom Love 发布了 Objective-C。Cox 的愿景是创造"软件集成电路"(Software ICs)——可复用的软件组件,就像硬件工程师使用集成电路一样(Cox, 1986)。
Objective-C 的设计哲学与 C++ 截然不同。如果说 C++ 是"给 C 加上 Simula 的类",那么 Objective-C 就是"给 C 加上 Smalltalk 的消息传递"。
Objective-C 的独特设计
Smalltalk 风格的消息传递:Objective-C 使用方括号语法发送消息:[object message:argument]。与 C++ 的虚函数调用不同,Objective-C 的消息发送是完全动态的——消息在运行时被解析,如果对象不理解某个消息,可以将其转发给其他对象。
C 的超集:Objective-C 是 C 的严格超集,任何合法的 C 程序都是合法的 Objective-C 程序。OOP 特性通过在 C 之上添加一个薄薄的运行时层来实现。
动态类型与静态类型的混合:Objective-C 支持 id 类型(类似于 Smalltalk 的动态类型)和具体类类型(静态类型)。程序员可以根据需要在两者之间切换。
协议(Protocol):Objective-C 的协议类似于 Java 的接口,定义了一组方法签名而不提供实现。但 Objective-C 的协议可以包含可选方法(optional method),这在 Java 接口中是不可能的(至少在 Java 8 之前)。
分类(Category):Objective-C 允许在不修改原始类源代码的情况下,向已有类添加新方法。这是一种强大的扩展机制,后来影响了 Swift 的 Extension、Kotlin 的扩展函数和 C# 的扩展方法。
Objective-C 的历史地位
Objective-C 长期处于小众地位,直到 1988 年 NeXT 公司(Steve Jobs 离开 Apple 后创立)选择它作为 NeXTSTEP 操作系统的主要开发语言。当 Apple 收购 NeXT 后,Objective-C 成为 macOS 和 iOS 开发的官方语言,迎来了第二春。
Objective-C 证明了一个重要观点:OOP 不一定需要 C++ 那样的静态、编译时绑定的方式来实现。动态消息传递虽然有运行时开销,但提供了极大的灵活性,使得 Cocoa 框架中的许多设计模式(如委托、KVO、响应链)成为可能。
Eiffel:契约式设计的先驱(1986)
Bertrand Meyer 的软件工程理想
1986 年,Bertrand Meyer 发布了 Eiffel 语言。Meyer 是一位严谨的软件工程学者,他的目标是创造一门能够支持可靠软件开发的语言。Eiffel 的设计深受 Meyer 自己的著作《面向对象软件构造》(Meyer, 1997)中阐述的原则影响。
Eiffel 的核心贡献
契约式设计(Design by Contract, DbC):这是 Eiffel 最重要的贡献。每个方法都可以声明:
- 前置条件(precondition):调用者必须满足的条件
- 后置条件(postcondition):方法执行后保证成立的条件
- 类不变量(class invariant):对象在任何公开可见的时刻都必须满足的条件
这些契约在运行时被检查,违反契约会导致异常。DbC 将 Hoare 逻辑(Hoare, 1969)的思想引入了 OOP 实践。
命令-查询分离原则(Command-Query Separation, CQS):Meyer 提出,一个方法要么是命令(改变对象状态,不返回值),要么是查询(返回值,不改变状态),不应该两者兼具。这一原则后来影响了 CQRS(命令查询职责分离)架构模式。
统一访问原则(Uniform Access Principle):属性和无参方法在语法上不可区分,调用者不需要知道一个值是存储的还是计算的。这一原则后来被 Kotlin、Scala 等语言采纳。
多重继承与特性重命名:Eiffel 支持多重继承,但通过特性重命名(feature renaming)和选择性继承(select)来解决命名冲突,而不是像 C++ 那样依赖虚继承。
Eiffel 虽然从未成为主流语言,但它的思想——特别是契约式设计——深刻影响了后来的语言设计和软件工程实践。
CLOS:多方法与元对象协议(1988)
Lisp 世界的面向对象
Common Lisp Object System(CLOS) 于 1988 年被纳入 ANSI Common Lisp 标准,它代表了 OOP 的另一种可能性。
CLOS 最独特的特性是多方法(multimethod)或泛函数(generic function):方法不属于任何一个类,而是根据所有参数的类型进行分派。在传统的 OOP 中,animal.speak() 只根据 animal 的类型进行分派(单分派);在 CLOS 中,(speak animal environment) 可以根据 animal 和 environment 两个参数的类型组合来选择具体实现(多分派)。
CLOS 还引入了元对象协议(Meta-Object Protocol, MOP),允许程序员自定义类的创建过程、方法分派机制、继承规则等。这是元编程的极致——你可以用 OOP 来重新定义 OOP 本身。
CLOS 的设计表明:OOP 不一定要把方法绑定在对象上。这一洞见后来在 Clojure 的多方法和 Julia 的多重分派中得到了回响。
BETA:模式与嵌套(1983–1993)
Simula 的精神继承者
BETA 语言由 Birger Møller-Pedersen、Kristen Nygaard 等人设计,是 Simula 的精神继承者(Madsen et al., 1993)。BETA 引入了一个统一的概念——模式(pattern),用来统一类、方法、异常、协程等概念。
BETA 的独特之处在于它的嵌套类(nested class)和虚模式(virtual pattern)。在 BETA 中,类可以嵌套在其他类中,内部类可以访问外部类的状态。这种设计后来影响了 Java 的内部类机制。
BETA 还引入了内部方法调用(inner call)——子类的方法可以调用父类方法中的 inner 标记点,这与传统的 super 调用方向相反。这种设计强制了一种"框架优先"的编程风格,父类定义算法骨架,子类填充细节。
Java:面向对象的工业化(1995)
James Gosling 的简化之道
1995 年,Sun Microsystems 发布了 Java。Java 的设计者 James Gosling 和他的团队从 C++ 的复杂性中吸取了教训,做出了一系列简化决策(Gosling et al., 2000):
去掉多重继承:Java 只支持单继承(一个类只能 extends 一个父类),但引入了接口(interface)来实现多重类型继承。接口只定义方法签名,不包含实现(直到 Java 8 引入默认方法)。这是对 C++ 多重继承问题的一个优雅回应。
去掉指针算术:Java 中所有对象都通过引用访问,没有指针算术,没有手动内存管理。垃圾回收(garbage collection)自动管理对象的生命周期。
去掉运算符重载:Java 不支持用户定义的运算符重载(String 的 + 是编译器特殊处理的),以避免滥用导致的代码可读性问题。
去掉头文件:Java 的编译单元是类文件,不需要 C/C++ 那样的头文件/实现文件分离。
引入接口:Java 的接口是一种纯抽象类型,只定义契约而不提供实现。这实现了接口与实现的分离,是《设计模式》中"面向接口编程"原则的语言级支持。
Java 的类型系统设计
Java 做了一个有趣的妥协:基本类型不是对象。int、boolean、double 等基本类型不继承自 Object,不能参与多态。这是出于性能考虑——将整数装箱为对象会带来显著的内存和性能开销。但这也破坏了"一切皆对象"的纯粹性,导致了自动装箱/拆箱(autoboxing/unboxing)等后续补丁。
Java 的泛型(Java 5, 2004)采用了类型擦除(type erasure)实现,即泛型类型信息在编译后被擦除。这是为了保持与旧版本 Java 字节码的向后兼容性,但也导致了许多令人困惑的限制(如无法创建泛型数组、无法在运行时获取泛型类型参数等)。
Java 的历史意义
Java 的成功不仅在于语言设计,更在于其生态系统:
- JVM(Java 虚拟机):提供了跨平台能力和优秀的运行时优化(HotSpot JIT 编译器的技术直接源自 Self 语言的研究)
- 丰富的标准库:从集合框架到并发工具,Java 提供了工业级的基础设施
- 企业级框架:Spring、Hibernate 等框架将 OOP 的设计模式工业化
Java 证明了一个重要观点:一门成功的 OOP 语言不需要是最纯粹或最强大的,它需要在表达力和简单性之间找到正确的平衡点。
Ruby:面向对象的快乐编程(1995)
Matz 的设计哲学
1995 年(与 Java 同年),松本行弘(Yukihiro “Matz” Matsumoto)发布了 Ruby。Matz 的设计哲学可以用一句话概括:为程序员的快乐而优化(Matsumoto, 2001)。
Matz 明确表示,Ruby 的主要灵感来源是 Smalltalk、Perl 和 Lisp。他想要 Smalltalk 的纯粹面向对象、Perl 的实用性和 Lisp 的表达力。
Ruby 的 OOP 特性
一切皆对象(真正的):与 Java 不同,Ruby 中的一切确实都是对象。1 是 Integer 的实例,nil 是 NilClass 的实例,true 是 TrueClass 的实例。你可以调用 1.even?、nil.nil?。
Mixin 模块:Ruby 通过 Module 和 include/extend 机制实现了代码复用,这是对多重继承问题的另一种解决方案。Module 类似于只包含方法的抽象类,可以被"混入"(mix in)到任何类中。这种机制比 Java 的接口更强大(因为 Module 可以包含实现),比 C++ 的多重继承更安全(因为 Module 不能被实例化,避免了状态冲突)。
开放类(Open Class):Ruby 允许在运行时重新打开任何类并添加或修改方法,包括内置类。你可以给 String 类添加新方法,甚至修改 Integer 的 + 方法。这种能力被称为"猴子补丁"(monkey patching),极其强大但也极其危险。
块(Block)与闭包:Ruby 的块是一种轻量级的闭包,几乎所有的迭代和回调都通过块来实现。[1,2,3].each { |x| puts x } 这种风格使得 Ruby 代码极其简洁和表达力强。
鸭子类型(Duck Typing):Ruby 奉行"如果它走起来像鸭子、叫起来像鸭子,那它就是鸭子"的哲学。对象的类型由其行为(它响应哪些方法)决定,而非由其类层次决定。这与 Smalltalk 的动态类型哲学一脉相承。
元编程:Ruby 提供了强大的元编程能力——method_missing、define_method、class_eval 等机制使得程序可以在运行时动态定义类和方法。Rails 框架大量使用这些特性来实现其"约定优于配置"的魔法。
Ruby 的取舍
Ruby 为了程序员的表达力和快乐,牺牲了:
- 性能:Ruby(特别是 MRI/CRuby)的执行速度长期落后于 Java 和 C++
- 类型安全:动态类型和鸭子类型意味着许多错误只能在运行时发现
- 可预测性:开放类和元编程使得代码的行为难以静态推理
JavaScript:原型继承的意外胜利(1995)
Brendan Eich 的十天奇迹
1995 年 5 月,Brendan Eich 在 Netscape 公司用大约十天时间设计并实现了 JavaScript(最初名为 Mocha,后改名 LiveScript,最终定名 JavaScript)。这个仓促的诞生过程深刻影响了语言的设计。
Eich 最初被要求设计一门"看起来像 Java"的脚本语言(这是 Netscape 与 Sun 的商业协议),但他个人更倾向于 Scheme(一种 Lisp 方言)和 Self 的设计理念。最终的 JavaScript 是一个奇特的混合体(Flanagan, 2020):
- 语法来自 Java/C
- 函数作为一等公民来自 Scheme
- 原型继承来自 Self
- 动态类型来自多种动态语言
JavaScript 的原型继承
JavaScript 的对象系统是基于原型的,这直接源自 Self 语言:
没有类(ES5 及之前):JavaScript 最初没有 class 关键字。对象通过构造函数(constructor function)和 new 运算符创建,通过原型链(prototype chain)实现继承。
原型链:每个对象都有一个内部链接指向另一个对象(其原型)。当访问一个属性时,如果对象本身没有该属性,JavaScript 引擎会沿着原型链向上查找,直到找到该属性或到达原型链的末端(null)。
函数即构造函数:任何函数都可以用作构造函数。function Dog(name) { this.name = name; } 配合 new Dog("Rex") 就创建了一个对象。方法通过 Dog.prototype.bark = function() { ... } 添加到原型上,被所有实例共享。
ES6 的 class 语法糖
2015 年的 ECMAScript 6(ES6)引入了 class 语法:
1 | |
但这只是语法糖——底层仍然是原型继承。class 关键字没有引入新的对象模型,只是让原型继承的写法更接近传统的基于类的 OOP 语言。
JavaScript 的 OOP 取舍
灵活性 vs 安全性:JavaScript 的对象是完全开放的——你可以在任何时候给任何对象添加或删除属性。这提供了极大的灵活性,但也意味着拼写错误不会被捕获(obj.nmae = "John" 不会报错,只会创建一个新属性)。
原型 vs 类:原型继承比类继承更灵活(对象可以直接继承自其他对象,不需要先定义类),但也更难理解和调试。Douglas Crockford 在《JavaScript: The Good Parts》中建议完全避免使用 new 和原型继承,转而使用对象组合和闭包来实现封装(Crockford, 2008)。
this 的困惑:JavaScript 的 this 关键字的绑定规则是出了名的复杂——它取决于函数的调用方式而非定义位置。这是 JavaScript 最大的设计缺陷之一,ES6 的箭头函数(arrow function)部分缓解了这个问题。
JavaScript 的历史意义
JavaScript 是历史上最广泛部署的编程语言——它运行在每一个 Web 浏览器中。JavaScript 的成功证明了:一门语言的流行程度与其设计质量之间没有必然联系。JavaScript 的许多设计决策是仓促和妥协的产物,但它的无处不在(ubiquity)使得它成为了事实上的通用编程语言。
JavaScript 也证明了原型继承是一种可行的(如果不是理想的)OOP 模型。尽管大多数 JavaScript 程序员最终通过 ES6 的 class 语法回到了类似基于类的编程风格,但原型继承的灵活性在某些场景下(如 mixin、动态代理)仍然是有价值的。
Python:实用主义的多范式(1991)
Guido van Rossum 的设计选择
虽然 Python 诞生于 1991 年(早于 Java 和 Ruby),但它对 OOP 的处理方式值得在这个时间点讨论,因为它代表了一种务实的多范式立场。
Python 的创造者 Guido van Rossum 受到了 ABC 语言、C、Modula-3 和 Unix shell 的影响。Python 支持 OOP,但不强制使用——你可以写纯过程式的 Python 代码,也可以写函数式风格的代码。
Python 的 OOP 特性
显式的 self:Python 的方法必须显式地将 self 作为第一个参数。这是一个有意的设计选择——它使得方法调用的机制透明化,避免了 JavaScript 中 this 绑定的困惑。
多重继承与 MRO:Python 支持多重继承,并通过 C3 线性化算法(C3 linearization)来确定方法解析顺序(Method Resolution Order, MRO),这比 C++ 的多重继承规则更加可预测。
鸭子类型:Python 与 Ruby 一样奉行鸭子类型哲学。Python 的 for 循环不要求对象是某个特定类的实例,只要求它实现了 __iter__ 方法。
魔术方法(Dunder Methods):Python 通过 __init__、__str__、__add__ 等双下划线方法来实现运算符重载和特殊行为。这是一种优雅的设计——运算符重载通过命名约定而非特殊语法来实现。
描述符协议(Descriptor Protocol):Python 的属性访问机制通过描述符协议实现,这是一种元编程机制,使得 property、classmethod、staticmethod 等装饰器成为可能。
C#:Java 的反思与超越(2000)
Anders Hejlsberg 的改进
2000 年,微软发布了 C#,由 Anders Hejlsberg(Turbo Pascal 和 Delphi 的设计者)主导设计。C# 在很大程度上是对 Java 的反思和改进:
- 属性(Property):C# 将 getter/setter 提升为语言级特性,而不是像 Java 那样依赖命名约定
- 委托(Delegate)与事件(Event):类型安全的函数指针,比 Java 的匿名内部类更优雅
- 值类型(struct):C# 区分引用类型(class)和值类型(struct),允许在栈上分配小型对象以提高性能
- 泛型的具化实现:与 Java 的类型擦除不同,C# 的泛型在运行时保留类型信息
- LINQ:语言集成查询,将函数式编程的思想融入 OOP
- 扩展方法:允许在不修改类源代码的情况下添加方法,类似 Objective-C 的分类
C# 的演化速度远快于 Java,它不断吸收其他语言的优秀特性(模式匹配、记录类型、异步编程等),成为了一门高度现代化的多范式语言。
Scala:OOP 与 FP 的融合(2004)
Martin Odersky 的统一愿景
2004 年,Martin Odersky 发布了 Scala(Scalable Language)。Odersky 曾参与 Java 泛型的设计,他对 Java 的局限性有深刻的理解。Scala 的目标是将面向对象编程和函数式编程统一在一个一致的框架中。
Scala 的 OOP 创新
Trait:Scala 的 trait 是一种可以包含具体实现的接口,支持可堆叠的修改(stackable modifications)。Trait 解决了多重继承的问题,同时比 Java 接口更强大、比 C++ 多重继承更安全。Trait 的设计后来影响了 Java 8 的默认方法、Rust 的 trait 和 Kotlin 的接口。
一切皆对象,一切皆表达式:Scala 中没有基本类型,1 是 Int 的实例。同时,if-else、match、try-catch 都是表达式,有返回值。
伴生对象(Companion Object):Scala 用伴生对象取代了 Java 的 static 成员。伴生对象本身是一个单例对象,与同名类共享私有访问权限。这消除了 OOP 中"静态成员不属于任何对象"的不一致性。
路径依赖类型(Path-Dependent Type):Scala 的类型系统支持路径依赖类型,即一个类型可以依赖于特定对象实例。这提供了比 Java 泛型更强大的类型表达能力。
Go:面向对象的解构(2009)
Rob Pike 的极简主义
2009 年,Google 发布了 Go(又称 Golang),由 Rob Pike、Ken Thompson 和 Robert Griesemer 设计。Go 的设计哲学是对 C++ 和 Java 复杂性的直接反叛。Rob Pike 在其著名的博客文章"Less is exponentially more"中写道(Pike, 2012):
“If C++ and Java are about type hierarchies and the taxonomy of types, Go is about composition.”
(如果说 C++ 和 Java 关注的是类型层次和类型分类学,那么 Go 关注的是组合。)
Go 的 OOP 特性(或者说,缺失的特性)
Go 对传统 OOP 进行了激进的解构:
没有类:Go 没有 class 关键字。方法可以附加到任何命名类型上:
1 | |
没有继承:Go 完全没有继承机制。代码复用通过组合(composition)和嵌入(embedding)实现:
1 | |
嵌入的类型的方法会被"提升"(promoted)到外部类型,看起来像继承,但语义不同——没有子类型关系,没有方法覆盖,没有 super 调用。
隐式接口:Go 的接口是隐式实现的——一个类型不需要声明它实现了某个接口,只要它拥有接口要求的所有方法,它就自动满足该接口。这被称为结构化类型(structural typing)或鸭子类型的静态版本:
1 | |
没有泛型(直到 Go 1.18):Go 在很长一段时间内没有泛型,这是一个有意的简化决策。Go 1.18(2022)最终引入了泛型,但其设计比 Java 或 C++ 的泛型简单得多。
Go 的设计哲学分析
Go 的 OOP 设计体现了几个重要的思想转变:
组合优于继承的极端化:《设计模式》提出"优先使用组合而非继承",Go 将这一原则推到了极致——它根本不提供继承。这迫使程序员使用组合和接口来组织代码,避免了深层继承层次带来的脆弱性。
隐式接口的力量:Go 的隐式接口消除了"实现一个接口需要修改类型定义"的耦合。你可以为第三方库的类型定义接口,而不需要修改其源代码。这实现了真正的依赖倒置——接口由使用者定义,而非提供者定义。
简单性作为特性:Go 的设计者认为,语言的复杂性是软件工程的敌人。Go 故意省略了许多"高级"特性(继承、异常、泛型、断言),以换取更低的学习曲线和更一致的代码风格。
Go 的争议
Go 的极简主义也招致了批评:
- 缺乏泛型(Go 1.18 之前)导致大量的代码重复和
interface{}的滥用 - 没有枚举类型、没有不可变性保证
- 错误处理通过返回值而非异常,导致大量的
if err != nil样板代码
但 Go 的成功证明了:在特定的应用领域(如云基础设施、微服务),简单性和可预测性比表达力更重要。
Rust:所有权与 Trait(2010–2015)
没有继承的类型安全 OOP
2010 年由 Graydon Hoare 在 Mozilla 启动、2015 年发布 1.0 版本的 Rust,在 OOP 方面做出了与 Go 类似但更加精细的选择:
Trait 系统:Rust 的 trait 类似于 Haskell 的 type class,定义了一组方法签名和可选的默认实现。类型通过 impl Trait for Type 显式实现 trait。与 Go 的隐式接口不同,Rust 要求显式声明。
没有继承,只有 Trait 组合:Rust 没有类继承。代码复用通过 trait 的默认方法、泛型和组合来实现。
所有权系统:Rust 最独特的贡献是其所有权(ownership)和借用(borrowing)系统,这在编译时保证了内存安全和线程安全,无需垃圾回收。这不是传统意义上的 OOP 特性,但它深刻影响了对象的生命周期管理方式。
枚举与模式匹配:Rust 的枚举(enum)是代数数据类型(algebraic data type),配合模式匹配,提供了一种与 OOP 多态互补的多态机制。
Rust 证明了:安全性、性能和抽象能力可以同时实现,但代价是更陡峭的学习曲线。
Kotlin 与 Swift:现代 OOP 的成熟(2011–2014)
吸取历史教训的新语言
Kotlin(2011, JetBrains)和 Swift(2014, Apple)代表了 OOP 语言设计的最新成果。它们都是在充分研究了前辈语言的优缺点后设计的:
Kotlin 的改进:
- 空安全类型系统:将可空性编码到类型系统中(
StringvsString?),在编译时消除空指针异常 - 数据类(data class):自动生成
equals、hashCode、toString、copy方法 - 密封类(sealed class):限制继承层次,配合
when表达式实现穷举检查 - 扩展函数:在不修改类的情况下添加方法,灵感来自 C# 和 Objective-C 的分类
- 协程:轻量级并发,不依赖线程
Swift 的改进:
- 值类型优先:Swift 的
struct是值类型,class是引用类型,鼓励使用值类型 - 协议导向编程(Protocol-Oriented Programming):Swift 2.0 提出了"协议导向编程"范式,将协议(类似接口/trait)作为代码组织的核心单元
- 可选类型(Optional):与 Kotlin 类似的空安全机制
- 枚举与关联值:Swift 的枚举可以携带关联数据,类似 Rust 的代数数据类型
TypeScript:为 JavaScript 添加类型(2012)
Anders Hejlsberg 的又一力作
2012 年,微软发布了 TypeScript,同样由 Anders Hejlsberg 主导设计。TypeScript 是 JavaScript 的超集,添加了静态类型系统。
TypeScript 的类型系统是结构化的(structural)而非名义的(nominal)——两个类型是否兼容取决于它们的结构(拥有哪些属性和方法),而非它们的名称或声明位置。这与 Go 的隐式接口哲学一致,也与 JavaScript 的鸭子类型传统相符。
TypeScript 还引入了联合类型(union type)、交叉类型(intersection type)、条件类型(conditional type)等高级类型特性,使得其类型系统成为了一种强大的类型级编程语言。
历史的回顾:OOP 的核心争论
纵观面向对象编程的半个世纪历史,几个核心争论贯穿始终:
类 vs 原型
基于类的 OOP(Simula、C++、Java、C#)将类作为对象的模板,强调类型层次和分类学。基于原型的 OOP(Self、JavaScript、Lua)将对象作为直接的实体,通过克隆和委托实现复用。
William Cook 在其重要论文"On Understanding Data Abstraction, Revisited"(Cook, 2009)中指出,这两种模型对应着两种不同的抽象方式:抽象数据类型(ADT)和对象(Object)。ADT 通过类型来组织操作,对象通过操作来组织类型。两者各有优势,不可互相替代。
继承 vs 组合
继承是 OOP 最具争议的特性。它提供了代码复用和多态,但也带来了脆弱基类问题(fragile base class problem)和紧耦合。从 C++ 的多重继承,到 Java 的单继承 + 接口,到 Ruby 的 Mixin,到 Go 的完全放弃继承,语言设计者们一直在寻找继承的替代方案。
历史的趋势是明确的:组合正在赢得这场争论。现代语言(Go、Rust、Kotlin)越来越倾向于通过接口/trait + 组合来实现代码复用,而非通过继承层次。
静态类型 vs 动态类型
静态类型(C++、Java、Go、Rust)在编译时捕获类型错误,提供更好的工具支持和性能优化机会。动态类型(Smalltalk、Ruby、Python、JavaScript)提供更大的灵活性和更快的开发速度。
近年来的趋势是渐进类型(gradual typing)——TypeScript 为 JavaScript 添加可选的静态类型,Python 3.5+ 引入类型注解,Ruby 3.0 引入 RBS 类型签名。这表明业界正在寻找两者之间的平衡点。
消息传递 vs 方法调用
Alan Kay 的原始 OOP 愿景强调消息传递——对象之间通过消息通信,接收者决定如何处理消息。大多数主流语言(C++、Java)将消息传递简化为方法调用——编译器在编译时(或通过虚表在运行时)确定调用哪个方法。
Objective-C 和 Smalltalk 保留了真正的消息传递语义,这使得它们支持消息转发、方法缺失处理等动态特性。Erlang/OTP 的 Actor 模型则将消息传递推向了进程间通信的层面(Armstrong, 2003),而 Go 的 goroutine + channel 模型受到了 Hoare 的 CSP(Communicating Sequential Processes)理论的启发(Hoare, 1978),代表了另一种"通过通信来共享"的哲学。
结语:OOP 的未来
面向对象编程不是一个静止的概念,而是一个不断演化的思想谱系。从 Simula 的类和继承,到 Smalltalk 的消息传递,到 Self 的原型,到 Go 的隐式接口,到 Rust 的 trait 和所有权——每一代语言都在重新定义"面向对象"的含义。
当代的趋势表明,未来的编程语言不会是"纯面向对象"的,而是多范式的。OOP 的核心洞见——封装(将数据和行为绑定在一起)、多态(同一接口的不同实现)、抽象(隐藏实现细节)——将继续存在,但它们的实现方式会越来越多样化。
正如 Alan Kay 所说:
“The big idea is messaging.”
(真正重要的思想是消息传递。)
也许,面向对象编程最持久的遗产不是类、继承或多态,而是一种思考方式——将复杂系统分解为相互协作的自治实体。这种思考方式,无论以何种语法形式表达,都将继续指导我们构建软件。
| 语言 | 年份 | OOP 模型 | 类型系统 | 继承方式 | 核心创新 |
|---|---|---|---|---|---|
| Simula 67 | 1967 | 基于类 | 静态 | 单继承 | 类、对象、虚方法 |
| Smalltalk-80 | 1980 | 基于类 | 动态 | 单继承 | 一切皆对象、消息传递 |
| Self | 1987 | 基于原型 | 动态 | 委托 | 原型继承、JIT 编译 |
| C++ | 1985 | 基于类 | 静态 | 多重继承 | 零开销抽象、模板 |
| Objective-C | 1984 | 基于类 | 混合 | 单继承 | 动态消息传递、分类 |
| Eiffel | 1986 | 基于类 | 静态 | 多重继承 | 契约式设计 |
| CLOS | 1988 | 泛函数 | 动态 | 多重继承 | 多方法、元对象协议 |
| Python | 1991 | 基于类 | 动态 | 多重继承(C3) | 鸭子类型、魔术方法 |
| Java | 1995 | 基于类 | 静态 | 单继承+接口 | JVM、垃圾回收 |
| Ruby | 1995 | 基于类 | 动态 | 单继承+Mixin | 开放类、元编程 |
| JavaScript | 1995 | 基于原型 | 动态 | 原型链 | 无处不在、闭包 |
| C# | 2000 | 基于类 | 静态 | 单继承+接口 | 属性、LINQ、泛型具化 |
| Scala | 2004 | 基于类 | 静态 | Trait | OOP+FP 统一 |
| Go | 2009 | 结构化 | 静态 | 无继承(组合) | 隐式接口、goroutine |
| Rust | 2015 | Trait | 静态 | 无继承(Trait) | 所有权、借用检查 |
| Kotlin | 2011 | 基于类 | 静态 | 单继承+接口 | 空安全、协程 |
| Swift | 2014 | 基于类 | 静态 | 单继承+协议 | 协议导向编程 |
| TypeScript | 2012 | 基于类/原型 | 静态(结构化) | 单继承+接口 | 结构化类型、渐进类型 |
参考资料
- Nygaard, K. & Dahl, O.-J. (1978). “The Development of the SIMULA Languages.” History of Programming Languages, ACM.
- Kay, A. (1993). “The Early History of Smalltalk.” ACM SIGPLAN Notices, 28(3).
- Stroustrup, B. (1993). “A History of C++: 1979–1991.” History of Programming Languages II, ACM.
- Cox, B. (1986). Object-Oriented Programming: An Evolutionary Approach. Addison-Wesley.
- Gosling, J. et al. (2000). The Java Language Specification, 2nd ed. Addison-Wesley.
- Goldberg, A. & Robson, D. (1983). Smalltalk-80: The Language and Its Implementation. Addison-Wesley.
- Matsumoto, Y. (2001). “The Ruby Programming Language.” Informatics Research for Development of Knowledge Society Infrastructure (ICKS).
- Flanagan, D. (2020). JavaScript: The Definitive Guide, 7th ed. O’Reilly.
- Crockford, D. (2008). JavaScript: The Good Parts. O’Reilly.
- Donovan, A. & Kernighan, B. (2015). The Go Programming Language. Addison-Wesley.
- Pike, R. (2012). “Less is exponentially more.” Blog post.
- Gamma, E. et al. (1994). Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley.
- Meyer, B. (1997). Object-Oriented Software Construction, 2nd ed. Prentice Hall.
- Abadi, M. & Cardelli, L. (1996). A Theory of Objects. Springer.
- Ungar, D. & Smith, R. B. (1987). “Self: The Power of Simplicity.” ACM SIGPLAN Notices, 22(12).
- Madsen, O. L. et al. (1993). Object-Oriented Programming in the BETA Programming Language. Addison-Wesley.
- Ierusalimschy, R. et al. (2007). “The Evolution of Lua.” Proceedings of the Third ACM SIGPLAN Conference on History of Programming Languages.
- Cardelli, L. & Wegner, P. (1985). “On Understanding Types, Data Abstraction, and Polymorphism.” Computing Surveys, 17(4).
- Cook, W. R. (2009). “On Understanding Data Abstraction, Revisited.” OOPSLA '09.
- Armstrong, J. (2003). Programming Erlang. Pragmatic Bookshelf.
- Hoare, C. A. R. (1978). “Communicating Sequential Processes.” Communications of the ACM, 21(8).
