子类型关系
与其他面向对象语言一样,仓颉语言提供子类型关系和子类型多态。举例说明(不限于下述用例):
- 假设函数的形参是类型
T
,则函数调用时传入的参数的实际类型既可以是T
也可以是T
的子类型(严格地说,T
的子类型已经包括T
自身,下同)。 - 假设赋值表达式
=
左侧的变量的类型是T
,则=
右侧的表达式的实际类型既可以是T
也可以是T
的子类型。 - 假设函数定义中用户标注的返回类型是
T
,则函数体的类型(以及函数体内所有return
表达式的类型)既可以是T
也可以是T
的子类型。
那么如何判定两个类型是否存在子类型关系呢?下面我们对此展开说明。
继承 class 带来的子类型关系
继承 class 后,子类即为父类的子类型。如下代码中, Sub
即为 Super
的子类型。
open class Super { }
class Sub <: Super { }
实现接口带来的子类型关系
实现接口(含扩展实现)后,实现接口的类型即为接口的子类型。如下代码中,I3
是 I1
和 I2
的子类型, C
是 I1
的子类型, Int64
是 I2
的子类型:
interface I1 { }
interface I2 { }
interface I3 <: I1 & I2 { }
class C <: I1 { }
extend Int64 <: I2 { }
需要注意的是,部分跨扩展类型赋值后的类型向下转换场景(is
或 as
)暂不支持,可能出现判断失败,见如下示例:
// 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 <: C1
和 C4 <: 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 <: U1
且 S2 <: S1
(注意顺序)。例如下面的代码定义了两个函数 f : (U1) -> S2
和 g : (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 <: I1
,C <: I2
,以及 Bool <: I2
,但根据子类型的传递性,也隐式存在 C <: I1
以及 Bool <: I1
这两个子类型关系。
interface I1 { }
interface I2 <: I1 { }
class C <: I2 { }
extend Bool <: I2 { }
泛型类型的子类型关系
泛型类型间也有子类型关系,详见泛型类型的子类型关系章节。