关系的核心是可替换性

子类型关系通常写作S <: T,表示S的值可在要求T的环境中安全使用。这里的安全须相对于所讨论语言的类型规则和操作来理解,而不是声称任意业务行为都相同。在具有记录子类型的简单模型中,包含姓名、年龄、学号的记录,可以交给只读取姓名与年龄的函数;额外学号不会妨碍它完成已有操作。

这称为记录的宽度子类型。它说明关系可由数据结构及其可用操作建立,不必先出现类继承语法。类型系统还可允许把一个值以较一般的类型看待,暂时不使用已知的额外能力。这不是删除对象中的字段,也不是对两个类型名称作文字上的包含判断。

参考:[1]

函数的输入与输出方向不同

假设学生是人的子类型。能处理任意人的函数,也能接受学生,因此它可替代一个只要求接收学生的函数;反过来,只会处理学生的函数不能承诺接受所有人。这是参数位置的逆变。返回值则相反:承诺返回人的函数若实际返回学生,仍满足调用者需要,是返回位置的协变。

变型描述的是类型构造如何传递子类型关系,不是所有语言都采用同一套规则。例如Rust参考文档把其子类型主要限定在生命周期及高阶生命周期相关情形,不能据上述教学模型断言Rust中的任意两个结构体只因字段相似就有子类型关系。

参考:[1] [2]

可变字段为什么难以协变

只读与可写接口必须区分。设一个可读写盒子原本只允许存放学生,若把它当作可读写的“人盒子”,使用者就可能放入一个非学生的人;原持有者再按学生取出时,承诺被破坏。读出希望保留协变方向,写入却要求相反方向,两种能力合在一起常迫使参数保持不变。

Rust提供了一个具体而受限的实例:共享引用对其指向的类型参数协变,可变引用对该参数不变;但可变引用的生命周期参数仍协变。由此不能把“不变”粗略说成整个类型的每个参数都没有关系。字段是否可变、暴露何种读写能力,都必须进入判断。

参考:[2] [1]

继承和行为契约不是同一件事

继承可以复用或覆盖实现,子类型则关乎哪些替换被允许。即使方法名称和参数类型兼容,也没有自动证明业务契约成立。Liskov与Wing在1994年论文中以栈和队列说明:两者都能提供放入和取出操作,但依赖后进先出顺序的程序,不能仅因签名相同就改用先进先出的队列。

他们的行为子类型要求,从父类型规范证明的相关安全性质在子类型对象上仍成立,并关注状态不变式及历史性质。单看值域包含也不够:论文指出,32位整数并不因取值可放入64位整数范围,就自动满足后者所有操作承诺。类型规则防止某类类型错误,行为规范还约束操作含义;类层次设计需要分别交代这些保证。

参考:[3]