跳转到内容
← 返回先驱
语言与系统先驱1939–14 分钟阅读

芭芭拉·利斯科夫

Barbara Liskov

1968 年,芭芭拉·利斯科夫(Barbara Liskov)从斯坦福大学计算机科学系获得博士学位——她是美国最早从计算机科学系拿到博士学位的女性之一(更早的玛丽·肯尼斯·凯勒 Mary Kenneth Keller 于 1965 年在威斯康星大学取得,常被列为美国第一位计算机科学博士)。她的博士论文研究的是下国际象棋…

抽象数据类型Liskov替换原则CLU语言面向对象分布式计算

1968 年,芭芭拉·利斯科夫(Barbara Liskov)从斯坦福大学计算机科学系获得博士学位——她是美国最早从计算机科学系拿到博士学位的女性之一(更早的玛丽·肯尼斯·凯勒 Mary Kenneth Keller 于 1965 年在威斯康星大学取得,常被列为美国第一位计算机科学博士)。她的博士论文研究的是下国际象棋残局的程序,导师是人工智能先驱约翰·麦卡锡(John McCarthy)。

五十五年后,她依然在 MIT 从事研究,仍然活跃。她留给编程世界的遗产,藏在你每次写下 if (dog instanceof Animal) 或设计一个接口的时候——你用的正是她定义的规则。

破除误解:Liskov 替换原则不是"继承的语法规则"

很多程序员知道"Liskov 替换原则"(Liskov Substitution Principle,LSP),但把它理解为一条关于类继承的语法规定。这是误解。

利斯科夫的贡献远不止于此。她在 1974 年和 1980 年代的工作回答了一个更根本的问题:什么是类型?子类型应该满足什么条件才是真正的子类型,而不只是碰巧用了同一个名字的另一个类型?

她的回答不是语法规则,而是语义契约:如果 S 是 T 的子类型,那么在程序中所有使用 T 的地方,替换为 S 之后,程序的行为必须与原来一致。这是一个关于程序意义的陈述,而非关于语法的规定。

这个区分,在面向对象语言遍地泛滥、"继承"被滥用为代码复用工具的 1990 至 2010 年代,产生了深远的影响。

现场:1974 年的 MIT,CLU 的诞生

在 CLU 之前,利斯科夫已经在系统方向出过手。1972 年,她发表了 Venus 操作系统的设计——一个实验性的分时操作系统,用信号量组织进程同步。亲手写一个真实系统的经历,让她切身体会到没有结构约束的代码会混乱到什么程度。CLU 的设计动机,正是对这种混乱的回答。

1974 年,利斯科夫在 MIT 开始领导设计 CLU 语言(取自 cluster,簇)。设计 CLU 的动机不是学术好奇,而是一个非常实际的工程问题:为什么大型程序这么难维护、这么容易出 bug?

利斯科夫的诊断是:当时的编程语言把数据结构(如整数、数组)和操作这些数据的代码分离开来,程序的任意部分都可以直接操纵内部数据——没有边界,没有封装。一个函数修改了一个全局数组的某个字段,影响了完全不相干的另一个函数,这种错误极难追踪。

她的解决方案是抽象数据类型(Abstract Data Types,ADT):把数据和操作数据的方法绑定在一起,对外只暴露操作接口,隐藏内部实现细节。

CLU 的核心语法单元叫做 cluster(簇):

stack = cluster is push, pop, top, empty
    rep = array[int]
    push = proc(s: cvt, x: int)
        array[int]$addh(s, x)
    end push
    pop = proc(s: cvt) returns (int)
        ...
    end pop
end stack
```

这个设计在今天的程序员看来像极了 Java 的 class 或者 Python 的 class——那是因为几乎所有今天的面向对象语言,都在 CLU 奠定的思路上建造。

CLU 的具体贡献包括:

特性说明
抽象数据类型首个在语言层面系统支持 ADT 的语言
迭代器(Iterator)首创受控遍历的迭代器模式,Python for x in collection 的先祖
异常(Exception)把异常处理(exception handling)作为语言特性,而非临时约定
多返回值函数可以返回多个值(Go 语言继承了这一设计)
参数化类型泛型(Generic)的早期形式

表格里有两项值得展开。迭代器:在 CLU 之前,遍历一个集合意味着调用方必须知道它的内部结构——数组下标还是链表指针——这本身就破坏了抽象。CLU 的迭代器把"下一个元素是什么"封装在类型内部:实现用 yield 逐个产出元素,调用方只写 for x in collection,完全不接触表示细节。异常:CLU 把异常设计为类型接口的一部分——一个操作除了声明正常返回什么,还要声明它可能发出(signal)哪些异常;调用方可以就地处理,也可以继续传播。异常不是"出错就跳走"的临时手段,而是契约里写明的一种正常语义。这两条思想,今天分别活在 Python 的生成器和 Java 的受检异常里。

Liskov 替换原则:超越 OOP 的形式定义

1987 年,利斯科夫在一篇题为《数据抽象与层次》(Data Abstraction and Hierarchy)的演讲中,首次清晰表述了后来以她命名的原则。1994 年,她与耶内特·温(Jeannette Wing)在论文中给出了更严格的形式化表述:

直觉表述:如果对于每一个类型 T 的对象 o₁,都有一个类型 S 的对象 o₂,使得对所有使用 T 的程序 P,当用 o₂ 替换 o₁ 时,P 的行为不变;那么 S 是 T 的子类型。

用更简单的话说:子类型的实例,必须能无感知地替换父类型的实例,且程序的正确性不受影响。

这个原则的力量,在反例中最清晰:

反例:正方形是矩形的子类吗?

几何上,正方形(Square)是矩形(Rectangle)的特例。但在面向对象设计中,如果让 Square 继承 Rectangle,问题就出现了:

python
rect = Rectangle()
rect.set_width(5)
rect.set_height(3)
assert rect.area() == 15  # 矩形:宽*高=15,正确

如果把 Rectangle 替换为 Square: sq = Square() sq.set_width(5) # Square 的 set_width 会同时改变高度 sq.set_height(3) # 把高度改为3,但同时也把宽度改为3 assert sq.area() == 15 # 实际得到 9,断言失败! ```

Square 不满足 LSP,因为替换后程序行为改变了。结论:几何上的"是",不等于程序语义上的"是子类型"

这个例子揭示了继承的深层陷阱:语法上的继承很容易,语义上的正确子类型关系却需要认真设计。

利斯科夫与温的形式化把"行为一致"拆成了几条可检查的约束,核心是不对称的两条:子类型的前置条件只能放宽,不能收紧——调用方按父类型的要求传参,子类型不能偷偷提出更高要求;后置条件只能加强,不能减弱——父类型承诺的结果,子类型至少要兑现。此外还有父类型不变量必须保持,以及一条常被忽略的"历史约束":子类型不能引入父类型中不可能出现的状态变化方式——正方形改宽度时连动高度,违反的正是这一条。注意整套定义里没有出现"继承"二字:它约束的是行为,任何机制——接口实现、鸭子类型——只要满足这些约束,都构成合法的子类型关系。

分布式系统:Argus 与事务内存

1980 年代,利斯科夫将注意力转向分布式系统,领导开发了 Argus 编程语言。Argus 的核心问题是:当计算分布在多台机器上,机器可能随时崩溃,如何保证程序的正确性?

Argus 引入了原子行为(Atomic Actions)的概念——多个操作要么全部成功,要么全部回滚,即使中间有机器崩溃。这与数据库的事务(Transaction)概念高度相关,但应用到了更一般的分布式程序设计中。

Argus 的抽象单位叫守护者(guardian):一个封装持久状态、运行在单一节点上的对象,外部只能通过它导出的操作访问。跨节点的操作被组织成原子动作,且可以嵌套——子动作的失败可以由父动作捕获并补偿,而不必整体放弃。原子性的实现靠两阶段提交:所有参与者先表决"能否提交",协调者收齐赞成票后才正式生效,任何一环崩溃都触发回滚。这套"对象加嵌套事务"的组合,在 1980 年代就把今天微服务架构最头疼的两个问题——远程调用的部分失败、跨服务的一致性——摆到了语言层面来回答。

她还在 1990 年代研究了拜占庭容错(Byzantine Fault Tolerance)的实用化,与Miguel Castro合作提出了 PBFT 算法(Practical Byzantine Fault Tolerance,1999),这是第一个在实际系统中可用的拜占庭容错共识协议,成为今天区块链共识算法研究的基础。

荣誉与影响

年份荣誉
1968斯坦福大学计算机科学博士(美国最早从计算机科学系获得该学位的女性之一,导师 John McCarthy)
2004IEEE John von Neumann 奖章
2008ACM 图灵奖(颁奖词:"对编程语言与系统设计在实践与理论基础上的贡献,尤其是数据抽象、容错与分布式计算";2009 年 3 月颁发)
2012美国国家工程院院士

利斯科夫是历史上第二位获得图灵奖的女性(第一位是 Frances Allen,2006 年),也是为数不多的将理论研究与实际系统实现相结合的获奖者之一——她不只是提出了 ADT 的思想,还把它实现成了 CLU 编译器。

代价与争议

利斯科夫曾指出,CLU 对工业界的直接影响不如预期——尽管它的思想通过 Simula、Smalltalk 等语言间接影响了 C++ 和 Java,但 CLU 本身并没有广泛部署。她将此部分归因于当时的"类型恐惧症":工业界害怕强类型系统带来的复杂性,更偏爱 C 那样灵活但危险的弱类型风格。

LSP 在实践中也面临批评:严格遵守 LSP 有时会让继承层次变得过于复杂,或强迫使用组合(Composition)而非继承——而这恰恰又是软件工程中著名的争论:"继承 vs 组合"。利斯科夫的工作实际上为"优先使用组合"一派提供了理论支撑。

还有一个微妙之处:LSP 在形式上定义了子类型关系,但实际的面向对象语言(Java、C++ 等)并不强制执行 LSP——语言的类型系统允许你用继承创造出违反 LSP 的子类,编译器不会警告你。这意味着 LSP 是一条设计规则,而非语言约束,执行依赖于程序员的训练和代码审查。

跨域连接

  • 本质主义:把"正方形是矩形"当成子类型,是把日常语言里的"是"读成了本质包含。她的反例把判据换成用法:在所有使用父类型的上下文中都能顶替,才叫是。这是"意义即使用"落到可执行判据上的一次难得实例——判定不看定义,看替换之后程序还对不对。
  • 生命之树与系统发育:分类学曾按外观相似度嵌套,分子证据把它推翻了——相似不等于同源。继承层次犯的是同一个错:按"看起来像"建树,而不是按"可替换的性质"建树。共同教训是,层级结构的合法性必须由一条可检验的关系定义,否则它只是命名上的方便。
  • 信息不对称:信息不对称通常被当作市场失灵的来源,封装却是刻意制造并维持不对称:调用方不知道内部表示,实现方才有自由更换它。推论跟得很紧——一旦调用方开始依赖接口未曾承诺的可观察行为,这层不对称就漏了,替换成本立刻回到封装之前。
  • 社会契约:替换原则的形式是成对的义务:调用方不得依赖未被承诺的性质,实现方不得削弱已作出的承诺,违约的判定只看后果不看意图。它与契约论还有一个更实际的共同点——写下来不等于能执行。编译器并不检查替换原则,执行只能落在评审与测试上。
  • 软件测试:既然替换原则一般情况下不可判定,工程上只能逼近它:把父类型的测试套件原样跑在子类型上,通过即认为契约未被削弱。这条做法把一个语义命题落成了可执行手段,也划出了它的上限——测试覆盖不到的行为,替换性就无从谈起。

参考文献

  • Liskov, B. The Design of the Venus Operating System. Communications of the ACM 15(3), 1972.(她早期的操作系统工作)
  • Liskov, B. & Zilles, S. Programming with Abstract Data Types. ACM SIGPLAN Notices 9(4), 1974.(ADT 概念的开创性论文)
  • Liskov, B., Snyder, A., Atkinson, R. & Schaffert, C. Abstraction Mechanisms in CLU. Communications of the ACM 20(8), 1977.(CLU 抽象机制的正式描述)
  • Liskov, B. Data Abstraction and Hierarchy. ACM SIGPLAN Notices 23(5), 1988.(LSP 首次表述)
  • Liskov, B. & Wing, J. M. A Behavioral Notion of Subtyping. ACM TOPLAS 16(6), 1994.(LSP 的形式化定义)
  • Liskov, B. Distributed Programming in Argus. Communications of the ACM 31(3), 1988.(Argus 语言与分布式编程模型)
  • Liskov, B. & Guttag, J. Abstraction and Specification in Program Development. MIT Press, 1986.(数据抽象方法论教科书)
  • Castro, M. & Liskov, B. Practical Byzantine Fault Tolerance. OSDI 1999.