Week 11
子类型 Subtyping
-
本部分使用伪代码来讲解子类型相关知识
-
子类型的一个想法:更加灵活的类型系统,允许一个多类型数据结构(这里认为是类似ML的记录)拥有额外的域(相对于目标函数的参数类型而言,在具有额外域的参数传入时,允许类型检查,同时能够在运行中忽视这些额外域正常运行)
-
子类型可以在不改变现有类型检查规则的情况下增强类型系统的灵活性
-
保持子类型的独立性
- 因为一个现有的编程语言已经拥有一大堆类型规则,不希望因引入子类型而改变之
- 加入两个内容
- 使用
t1 <: t2
表示t1
是t2
的子类型 - 新的类型规则:如果
e
具有类型t1
且t1 <: t2
,那么e
具有类型t2
- 使用
-
子类型并不是一种观点和选择,其加入必须要确保类型系统的健全性(soundness)
-
可替换性(substitutability)原则,如果
t1 <: t2
那么任何t1
的值都可以按照任何t2
的值的使用方式被使用 -
四个子类型规则
- 宽度子类型(width subtyping):超类型有着所有同类型的域子集(相对于子类型)
- 置换子类型(permutation subtyping):超类型有着具有相同类型的域的不同顺序排列
- 传递性:如果有
t1 <: t2
且t2 <: t3
,那么有t1 <: t3
- 自反性:任一种类型都是其本身的子类型
-
一个想法:深度子类型(Depth Subtyping)?
- 我们允许宽度子类型的同时,允许其中一个域的子类型作为其域的累心
- 但是这种设计是不健全的
- 可能出现允许访问不存在的域的情况(我们认为这个域的其中一个部分存在,但是在某个过程中这个部分不存在了)
- 对于具备
getter
和setter
的语言,采用深度子类新是不健全的(可能存在上述问题) - 如果能够保证整个结构是不可异变的(中途的丢失不会发生),那么深度子类型是可异变的
函数子类型 Function Subtyping
- 让函数本身的类型是一个子类型:对于一个
t1 -> t2
的函数(高阶函数),允许传入一个t3 -> t4
的函数,而类型上存在子类型的性质 - 如果
ta <: tb
,那么t -> ta <: t -> tb
是成立的(可以返回一些无关的东西) - 函数的返回类型是协变的(covariant)
- 但是对于参数类型,是不允许子类型的,因为会不健全:如果
ta <: tb
,那么ta -> t <: tb -> t
是不被允许的(可能在调用该高阶函数时访问一些传入的父类型参数不包含的内容) - 另外一个思路是允许的:我们不能多访问一些未知内容,但是可以少访问一些已知内容:如果
tb <: ta
,那么ta -> t <; tb -> t
是成立的 - 函数的参数类型是逆变的(contravariant)
- 上述二者的结合:如果
t3 <: t1
且t2 <: t4
,那么t1 -> t2 <: t3 -> t4
是成立的 - 函数子类型在参数上逆变,同时在结果上协变
面向对象的子类型 Subtyping for OOP
- 用上述函数语言的子类型理解OOP语言的静态类检查
- 类比理解
- 类名就是类型
- 子类就是子类型
- 替换原则:子类的实例能够被用在任何超类出现的位置
- 对象几乎就是拥有域和方法的记录
- 域是可异变的(因此深度子类型不健全而不被允许)
- 方法是不可异变的,且完全就是函数的子类型规则,因此子类可以对方法进行覆盖
- 因此,可以使用子类型的思想设计类型系统
- 子类型可以拥有额外的域和方法
- 覆盖的方法要求参数逆变且结果协变
- 对于Java和C#的实际情况
- 类名即类型,子类型即显式子类(巧合的内容共性是不被允许的)
- 子类可以添加域和方法
- 子类可以覆盖方法,但只允许协变返回类型,参数不可变化(否则认为是重载)
- 类(Class)与类型(Type)
- 类定义了一个对象的行为,子类继承这些行为,并通过扩展和覆盖修改这些行为
- 类型描述了一个对象方法的参数及结果类型(接口),子类型即这些类型的可替代版本
- self / this非常特殊
- 对于类中定义的方法,实际上self / this是该方法的一个参数
- 尽管我们定义参数是逆变的,但是对于self / this,是协变的,即代表了当前类型的对象实例
- 这一特殊性是健全的,因为在调用时,我们往往考虑的是当前调用者本身的内容
泛型与子类型 Generics Versus Subtyping
- 泛型的擅长领域(参数多型)
- 混合了其他函数的函数的类型
- 在泛型集合上完成操作的函数的类型
- 泛型一般可以使用任何类型的情形,但是我们仍需要使用相同的记号限制类型之间的关系
- 同样在OO语言中适用,尽管有一些笨拙
- 子类型并不能应用于上述场景
- 就像Java处理泛型加入前的问题,即通用Object集合,虽然子类型保证了健全性,但这会导致想要真正使用实例时由于向下转型只会有动态运行时检查而非静态类型检查
- 子类型的擅长领域(子类型多型)
- 需要某一个内容但是对“远不止某内容”也友好
有界多型 Bounded Polymorphism
- 泛型和子类型的混合:任何是
t2
子类型的类型t1
- 使用子类型对泛型类型加以限制
- Java实现机制
<T extends Pt>