仓颉编程语言:子类型关系

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

  • 假设函数的形参是类型 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 { }

写在最后

  • 如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:
  • 点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。
  • 关注小编,同时可以期待后续文章ing ,不定期分享原创知识。

  • 13
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值