子类型关系

与其他面向对象语言一样,仓颉语言提供子类型关系和子类型多态。举例说明(不限于下述用例):

  • 假设函数的形参是类型 T,则函数调用时传入的参数的实际类型既可以是 T 也可以是 T 的子类型(严格地说,T 的子类型已经包括 T 自身,下同)。
  • 假设赋值表达式 = 左侧的变量的类型是 T,则 = 右侧的表达式的实际类型既可以是 T 也可以是 T 的子类型。
  • 假设函数定义中用户标注的返回类型是 T,则函数体的类型(以及函数体内所有 return 表达式的类型)既可以是 T 也可以是 T 的子类型。

那么如何判定两个类型是否存在子类型关系呢?下面我们对此展开说明。

继承 class 带来的子类型关系

继承 class 后,子类即为父类的子类型。如下代码中, Sub 即为 Super 的子类型。

open class Super { }
class Sub <: Super { }

实现接口带来的子类型关系

实现接口(含扩展实现)后,实现接口的类型即为接口的子类型。如下代码中,I3I1I2 的子类型, CI1 的子类型, Int64I2 的子类型:

interface I1 { }
interface I2 { }

interface I3 <: I1 & I2 { }

class C <: I1 { }

extend Int64 <: I2 { }

需要注意的是,部分跨扩展类型赋值后的类型向下转换场景(isas)暂不支持,可能出现判断失败,见如下示例:

// file1.cj
package p1

public class A{}

public func get(): Any {
    return A()
}

// =====================
// file2.cj
import p1.*

interface I0 {}

extend A <: I0 {}

main() {
    let v: Any = get()
    println(v is I0) // 无法正确判断类型,打印内容不确定
}

元组类型的子类型关系

仓颉语言中的元组类型也有子类型关系。直观的,如果一个元组 t1 的每个元素的类型都是另一个元组 t2 的对应位置元素类型的子类型,那么元组 t1 的类型也是元组 t2 的类型的子类型。例如下面的代码中,由于 C2 <: C1C4 <: C3,因此也有 (C2, C4) <: (C1, C3) 以及 (C4, C2) <: (C3, C1)

open class C1 { }
class C2 <: C1 { }

open class C3 { }
class C4 <: C3 { }

let t1: (C1, C3) = (C2(), C4()) // OK
let t2: (C3, C1) = (C4(), C2()) // OK

函数类型的子类型关系

仓颉语言中,函数是一等公民,而函数类型亦有子类型关系:给定两个函数类型 (U1) -> S2(U2) -> S1(U1) -> S2 <: (U2) -> S1 当且仅当 U2 <: U1S2 <: S1(注意顺序)。例如下面的代码定义了两个函数 f : (U1) -> S2g : (U2) -> S1,且 f 的类型是 g 的类型的子类型。由于 f 的类型是 g 的子类型,所以代码中使用到 g 的地方都可以换为 f

open class U1 { }
class U2 <: U1 { }

open class S1 { }
class S2 <: S1 { }


func f(a: U1): S2 { S2() }
func g(a: U2): S1 { S1() }

func call1() {
    g(U2()) // Ok.
    f(U2()) // Ok.
}

func h(lam: (U2) -> S1): S1 {
    lam(U2())
}

func call2() {
    h(g) // Ok.
    h(f) // Ok.
}

对于上面的规则,S2 <: S1 部分很好理解:函数调用产生的结果数据会被后续程序使用,函数 g 可以产生 S1 类型的结果数据,函数 f 可以产生 S2 类型的结果,而 g 产生的结果数据应当能被 f 产生的结果数据替代,因此要求 S2 <: S1

对于 U2 <: U1 的部分,可以这样理解:在函数调用产生结果前,它本身应当能够被调用,函数调用的实参类型固定不变,同时形参类型要求更宽松时,依然可以被调用,而形参类型要求更严格时可能无法被调用——例如给定上述代码中的定义 g(U2()) 可以被换为 f(U2()),正是因为实参类型 U2 的要求更严格于形参类型 U1

永远成立的子类型关系

仓颉语言中,有些预设的子类型关系是永远成立的:

  • 一个类型 T 永远是自身的子类型,即 T <: T
  • Nothing 类型永远是其他任意类型 T 的子类型,即 Nothing <: T
  • 任意类型 T 都是 Any 类型的子类型,即 T <: Any
  • 任意 class 定义的类型都是 Object 的子类型,即如果有 class C {},则 C <: Object

传递性带来的子类型关系

子类型关系具有传递性。如下代码中,虽然只描述了 I2 <: I1C <: I2,以及 Bool <: I2,但根据子类型的传递性,也隐式存在 C <: I1 以及 Bool <: I1 这两个子类型关系。

interface I1 { }
interface I2 <: I1 { }

class C <: I2 { }

extend Bool <: I2 { }

泛型类型的子类型关系

泛型类型间也有子类型关系,详见泛型类型的子类型关系章节。