为什么可变类型不是不可变类型的子类型?

Why isn't mutable a subtype of immutable, or vice versa?

在编程语言的类型系统中,可变(mutable)与不可变(immutable)数据结构为何不能互为子类型?这并非技术限制,而是为了严格遵守Liskov substitution principle。如果将可变类型视为不可变类型的子类型,当代码期望不可变对象时传入可变对象,后续对对象的修改会破坏原有的不变性契约,导致哈希值失效等严重问题。文章通过pair数据结构的例子,深入剖析了类型安全背后的逻辑,并探讨了如何通过type classes或duck typing等机制,在不牺牲类型安全的前提下实现灵活的多态调用。

我记得当我第一次了解到不可变性的那一刻,它改变了一切。
  1. js8

    我觉得这个解释有点过于复杂了。类型是值的属性,而不是变量的属性。可变性是变量的属性,而不是值的属性。

    所以这有点范畴错误。(我想在这里开个玩笑:在范畴论中,所有的范畴错误本质上都是类型错误。)当我们说“变量的类型”时,意思是这个变量只能被赋值(绑定)为特定类型的值。这与它是否可以被重新赋值(即可变性)毫无关系。

    所以你甚至不需要子类型的概念来解释这一点。

    此外,也许可以将变量定义为其类型上的一个 monad。

  2. bruce343434

    所以实际上有两个正交的维度:

    - A:我能否改变该值

    - B:其他东西能否改变该值(我能否依赖一个可预测的稳定值)

    由于这两个维度是正交的,基于层级的子类型(继承)会失效,但类型类(接口)和特设多态性仍然可行。

    在 C 语言中,const 解决了 A。

    在 Rust 中,由于指针别名限制(要么是一个可变指针,要么是任意数量的只读指针),(缺少)mut 同时解决了 A 和 B。

  3. BlackFly

    如果你将可变性与独占访问配对,那么你就处理了此处(有些人可能认为过于)严格的子类型定义所提出的异议,以及针对该异议的争论。任何请求不可变实例的代码都永远不会观察到可变性,因为请求不可变引用是独占的。因此,你可以传递可变引用,但只要某个东西持有该引用,可变性对其他代码就不再可用。

    所以,可变性异或别名提供了这种严格的子类型关系。当然,你还需要提供没有此类契约的对象来放宽这种限制,这就进入了内部可变性的领域,在那里,可变方法也可以被理解为子类型的一部分,因为持有没有可变方法的引用的持有者已明确被告知,无法保证对象不会发生变化。

  4. Sharlin

    作为一个完全离题的旁白,`car` 和 `cdr` 这两个用来表示“头”和“尾”的名字,老实说,是计算机历史上最令人困惑的遗留物之一。

  5. anankaie

    cons 的例子混淆了实现细节(返回传递给 cons 的值)和语义(返回由 cons 实例化的这个对的左侧)。在第一种情况下,思考可变性是一个范畴错误。在第二种情况下,可变性完全讲得通。

    此外,我怀疑可以构造一个接口,使得在静态上证明你可以将一个类型 Liskov 替换到该接口中,等价于判定停机问题:你只需要在类型系统中使用外延语义。

同日更多故事

2026-09-20