仓颉中的类型型变

类型型变

首先我们来看一下仓颉官网中关于类型型变的描述

如果 AB 是类型,T 是类型构造器,设其有一个类型参数 X,那么:

如果 T(A) <: T(B) 当且仅当 A <: B,则 TX 处是协变的。

如果 T(A) <: T(B) 当且仅当 B <: A,则 TX 处是逆变的。

如果 T(A) <: T(B) 当且仅当 A = B,则 T不型变

用继承关系来说就是

父类指针可以指向子类实例,称为协变。也就是顺着继承关系来

子类对象可以强制当做父类来用,称为逆变。也就是逆着继承关系来

不型变是说子类和父类各自独立,毫无关系

解释代码

cj 复制代码
// 父类
open class Parent {
    open public func printHello() {
        println("hello, I'm a parent")
    }
}
// 子类
class Child <: Parent {}

private func test() {
    var p = Parent()
    let c = Child()
    // 协变。父类指针指向子类对象
    p = c

    // 逆变。子类对象强转为父类类型,进行使用
    (c as Parent).getOrThrow().printHello()
}

泛型不型变

在仓颉编程语言中,所有的泛型都是不型变的 。这意味着如果AB的子类型,ClassName<A>ClassName<B>之间没有子类型关系。我们禁止这样的行为以保证运行时的安全。

泛型不型变对我们写代码时造成一些影响

下面的代码由于泛型不型变的存在,会在编译时就报错

报错代码

cj 复制代码
private func testArgs0() {
    let hashMap = HashMap([('key1', 'value1'), ('key2', 'value2')])

    let m1 = map{entry => "${entry[0]}:${entry[1]}"}(hashMap)
    let m2 = collectArray(m1)
    let m3 = ["aa", "bb", "cc"]

    let s = collectString(delimiter: "\n")

    let r1 = s(m1)
    let r2 = s(m1.iterator())
    
    let r3 = s(m2)
    let r4 = s(m2.iterator())

    let r5 = s(["aa", "bb", "cc"])
    let r6 = s(m3)
    let r7 = s(m3.iterator())
}

这里涉及到两个泛型函数collectArray和map。官网的API定义如下

可以看到map和collectArray都是泛型实现。我们来看下为什么报错

为什么报错

编译器推断的各个变量的类型如下。

  1. m1是Iterator<String> 类型
  2. m2和m3是Array<String> 类型
  3. s是(Iterable<ToString>):Struct-String 类型

这几个变量都是泛型类型,由于泛型不型变的存在,m1,m2,m3的String无法协变或逆变到ToString。所以会报错

r5能编译通过是由于编译器推断的类型是按照s所需参数的类型进行推断的,即(Iterable<ToString>):Struct-String。当然这里得益于["aa", "bb", "cc"]是一个字面量,它的类型才被编译器自动推断

解决方式

这是一个泛型不型变的问题,我们需要把参数的泛型类型和函数s的泛型类型保持一致即可,有两种修改方式

  1. 修改map和collectArray的泛型参数为ToString
cj 复制代码
private func testArgs0() {
    let hashMap = HashMap([('key1', 'value1'), ('key2', 'value2')])
    // Iterator<ToString>
    let m1 = map<(String,String),ToString>{entry => "${entry[0]}:${entry[1]}"}(hashMap)
    // Array<ToString>
    let m2 = collectArray<ToString>(m1)
    // Array<ToString>
    let m3: Array<ToString> = ["aa", "bb", "cc"]

    // (Iterable<ToString>):Struct-String
    let s = collectString(delimiter: "\n")

    let r1 = s(m1)
    let r2 = s(m1.iterator())
    
    let r3 = s(m2)
    let r4 = s(m2.iterator())

    let r5 = s(["aa", "bb", "cc"])
    let r6 = s(m3)
    let r7 = s(m3.iterator())
}
  1. 修改s的类型,和m1、m2、m3的泛型类型保持一致
cj 复制代码
private func testArgs0() {
    let hashMap = HashMap([('key1', 'value1'), ('key2', 'value2')])
    // Iterator<String>
    let m1 = map{entry => "${entry[0]}:${entry[1]}"}(hashMap)
    // Array<String>
    let m2 = collectArray(m1)
    // Array<String>
    let m3 = ["aa", "bb", "cc"]

    // (Iterable<String>):Struct-String
    let s = collectString<String>(delimiter: "\n")

    let r1 = s(m1)
    let r2 = s(m1.iterator())
    
    let r3 = s(m2)
    let r4 = s(m2.iterator())

    let r5 = s(["aa", "bb", "cc"])
    let r6 = s(m3)
    let r7 = s(m3.iterator())
}

可以看到无论哪种,最终都是将m1、m2、m3的泛型类型和s的泛型类型保持一致。

至于r5,我们始终没改动过,因为编译器会对字面量进行自动类型推断,推断的参考类型就是s的泛型形参的类型,所以r5能始终编译通过

至于s1(m)和s1(m.iterator())都可以编译通过,则是由于多态的存在。Array和Iterator都默认实现了Iterable接口

其他的子类型关系

继承 class 带来的子类型关系

继承 class 后,子类即为父类的子类型

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

实现接口(含扩展实现)后,实现接口的类型即为接口的子类型

函数类型的型变

函数的参数类型是逆变的,函数的返回类型是协变的

  1. 仓颉语言中,函数是一等公民,函数类型也有子类型关系
  2. 给定两个函数类型 (U1) -> S2(U2) -> S1
  3. (U1) -> S2 <: (U2) -> S1 当且仅当 U2 <: U1S2 <: S1(注意顺序)

例如下面的代码定义了两个函数 f : (U1) -> S2g : (U2) -> S1,且 f 的类型是 g 的类型的子类型。由于 f 的类型是 g 的子类型,所以代码中使用到 g 的地方都可以换为 f

cj 复制代码
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

元组类型的协变

元组之间是存在子类型关系的,如果一个元组的每一个元素都是另一个元组的对应位元素的子类型,则该元组是另一个元组的子类型。假设有元组 Tuple1Tuple2,它们的类型分别为 (A1, A2.., An)(B1, B2.., Bn),如果对于所有 i 都满足 Ai <: Bi,则 Tuple1 <: Tuple2

元组类型的子类型关系

仓颉语言中的元组类型也有子类型关系。直观的,如果一个元组 t1 的每个元素的类型都是另一个元组 t2 的对应位置元素类型的子类型,那么元组 t1 的类型也是元组 t2 的类型的子类型

永远成立的子类型关系

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

一个类型 T 永远是自身的子类型,即 T <: T

Nothing 类型永远是其他任意类型 T 的子类型,即 Nothing <: T

任意类型 T 都是 Any 类型的子类型,即 T <: Any

任意 class 定义的类型都是 Object 的子类型,即如果有 class C {},则 C <: Object

传递性带来的子类型关系

子类型关系具有传递性

参考资料

  1. 仓颉Spec 类型型变 cangjie-lang.cn/docs?url=%2...
  2. 泛型类型的子类型关系 cangjie-lang.cn/docs?url=%2...
  3. collection库中的函数 cangjie-lang.cn/docs?url=%2...
相关推荐
大G哥43 分钟前
鸿蒙NEXT开发中使用星闪服务
华为·harmonyos
马剑威(威哥爱编程)1 小时前
鸿蒙NEXT使用request模块实现本地文件上传
华为·harmonyos·harmonyos-next
轻口味2 小时前
【每日学点鸿蒙知识】tensorflowlite编译、音频编码线程、沉浸式状态栏、TextArea最大字节数限制等
华为·音视频·harmonyos
夜阑卧听风吹雨,铁马冰河入梦来2 小时前
Hypium纯血鸿蒙系统 HarmonyOS NEXT自动化测试框架
华为·harmonyos
我是Feri8 小时前
Harmony OS开发-ArkUI框架速成四
harmonyos·arkts·arkui
轻口味11 小时前
【每日学点鸿蒙知识】低功耗蓝牙、指纹识别认证、读取raw文件示例、CommonEvent是否跨线程、定位参数解释等
华为·harmonyos
御承扬11 小时前
从零开始开发纯血鸿蒙应用之实现内部文件处理页
华为·harmonyos·arkts·文本编辑·文本浏览
轻口味11 小时前
【每日学点鸿蒙知识】ASON工具、自定义tabbar、musl、Text异常截断等
华为·harmonyos
liuhaikang12 小时前
鸿蒙MPChart图表自定义(六)在图表中绘制游标
华为·harmonyos
万少1 天前
鸿蒙元服务实战-笑笑五子棋(1)
前端·harmonyos