类型型变
首先我们来看一下仓颉官网中关于类型型变的描述
如果
A和B是类型,T是类型构造器,设其有一个类型参数X,那么:如果
T(A) <: T(B)当且仅当A <: B,则T在X处是协变的。如果
T(A) <: T(B)当且仅当B <: A,则T在X处是逆变的。如果
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()
}泛型不型变
在仓颉编程语言中,所有的泛型都是不型变的 。这意味着如果
A是B的子类型,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都是泛型实现。我们来看下为什么报错
为什么报错
编译器推断的各个变量的类型如下。
- m1是Iterator<String>类型
- m2和m3是Array<String>类型
- s是(Iterable<ToString>):Struct-String类型
这几个变量都是泛型类型,由于泛型不型变的存在,m1,m2,m3的String无法协变或逆变到ToString。所以会报错
r5能编译通过是由于编译器推断的类型是按照s所需参数的类型进行推断的,即(Iterable<ToString>):Struct-String。当然这里得益于["aa", "bb", "cc"]是一个字面量,它的类型才被编译器自动推断
解决方式
这是一个泛型不型变的问题,我们需要把参数的泛型类型和函数s的泛型类型保持一致即可,有两种修改方式
- 修改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())
}- 修改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 后,子类即为父类的子类型
实现接口带来的子类型关系
实现接口(含扩展实现)后,实现接口的类型即为接口的子类型
函数类型的型变
函数的参数类型是逆变的,函数的返回类型是协变的
- 仓颉语言中,函数是一等公民,函数类型也有子类型关系
- 给定两个函数类型 (U1) -> S2和(U2) -> S1
- (U1) -> S2 <: (U2) -> S1当且仅当- U2 <: U1且- S2 <: S1(注意顺序)
例如下面的代码定义了两个函数 f : (U1) -> S2 和 g : (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 。
元组类型的协变
元组之间是存在子类型关系的,如果一个元组的每一个元素都是另一个元组的对应位元素的子类型,则该元组是另一个元组的子类型。假设有元组
Tuple1和Tuple2,它们的类型分别为(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
传递性带来的子类型关系
子类型关系具有传递性
参考资料
- 仓颉Spec 类型型变 cangjie-lang.cn/docs?url=%2...
- 泛型类型的子类型关系 cangjie-lang.cn/docs?url=%2...
- collection库中的函数 cangjie-lang.cn/docs?url=%2...