类型型变
首先我们来看一下仓颉官网中关于类型型变的描述
如果
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...