4205 字
21 分钟
仓颉文档阅读-开发指南VII: 类和接口(V) - 子类型关系 以及 类型转换
NOTE

阅读文档版本:

语言规约 Cangjie-0.53.18-Spec

具体开发指南 Cangjie-LTS-1.0.4

在阅读 了解仓颉的语言规约时, 难免会涉及到一些仓颉的示例代码, 但 我们对仓颉并不熟悉, 所以可以用 仓颉在线体验 快速验证

有条件当然可以直接 配置 Canjie-SDK

WARNING

博主在此之前, 基本只接触过 C/C++语言, 对大多现代语言都没有了解, 所以在阅读过程中遇到相似的概念, 难免会与 C/C++中的相似概念作类比, 见谅

且, 本系列是文档阅读, 而不是仓颉的零基础教学, 所以如果要跟着阅读的话最好有一门编程语言的开发经验

WARNING

在阅读仓颉编程语言的开发指南之前, 已经大概阅读了一遍 仓颉编程语言的语言规约, 已经对仓颉编程语言有了一个大概的了解

所以在阅读开发指南时, 不会对类似: 类、函数、结构体、接口等解释起来较为复杂名称 做出解释

此样式内容, 表示文档原文内容

类和接口#

子类型关系#

与其他面向对象语言一样, 仓颉语言提供子类型关系和子类型多态

举例说明(不限于下述用例):

  • 假设函数的形参是类型T, 则函数调用时传入的参数的实际类型既可以是T也可以是T的子类型(严格地说, T的子类型已经包括T自身, 下同)

  • 假设赋值表达式=左侧的变量的类型是T, 则=右侧的表达式的实际类型既可以是T也可以是T的子类型

  • 假设函数定义中用户标注的返回类型是T, 则函数体的类型(以及函数体内所有return表达式的类型)既可以是T也可以是T的子类型

下文将说明两个类型为子类型关系的几种情况

仓颉是强类型语言, 不同类型之间 是不允许存在 隐式类型转换 的

不过, 某些类型之间是可以通过显式类型转换语法进行转换的, 比如Int64Float64之间的转换

除此之外, 实际上 拥有父子类型关系的类型, 仓颉是允许隐式类型转换的

但, 只允许子类型转换为父类型, 父类型禁止转换为子类型

继承class带来的子类型关系#

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

如下代码中, Sub即为Super的子类型

open class Super { }
class Sub <: Super { }

存在继承关系的类型, 默认具有父子类型关系, 父类为父类型 子类为子类型

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

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

如下代码中, I3I1I2的子类型, CI1的子类型, Int64I2的子类型:

interface I1 { }
interface I2 { }
interface I3 <: I1 & I2 { }
class C <: I1 { }
extend Int64 <: I2 { }

一个类型实现了一个接口, 则此类型与接口之间默认具有父子类型关系, 接口为父类型 实现接口的类型为子类型

元组类型的子类型关系#

仓颉语言中的元组类型也有子类型关系

直观的, 如果一个元组t1的每个元素的类型都是另一个元组t2的对应位置元素类型的子类型, 那么元组t1的类型也是元组t2的类型的子类型

例如下面的代码中, 由于C2 <: C1C4 <: 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

元组类型也存在父子类型关系, 因为元组类型是基于其他数据类型的, 所以 当元组内 元素的类型均具有同”方向”的父子类型关系时, 则 元组类型之间也具有同”方向”的父子类型关系

什么意思呢?

如果存在元组类型: (Type1, Type2)(Type3, Type4), 如果Type1 <: Type3Type2 <: Type4, 则(Type1, Type2) <: (Type3, Type4)

元组类型之间如果想要存在父子类型关系, 需要保证 元组的所有元素类型, 按照顺序具有同”方向”的父子类型关系

函数类型的子类型关系#

仓颉语言中, 函数是一等公民, 而函数类型亦有子类型关系: 给定两个函数类型(U1) -> S2(U2) -> S1, 如果存在(U1) -> S2(U2) -> S1的子类型, 当且仅当U2U1的子类型, 且S2S1的子类型(注意顺序)

例如下面的代码定义了两个函数f : (U1) -> S2g : (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

仓颉是强类型语言, 函数是拥有具体类型的, 而函数类型之间同样可以具有父子类型关系

文档中的例子描述的有一些混乱

如果存在两个函数类型: (U1) -> S1(U2) -> S2

只有在, U1U2的子类型, 且S2S1的子类型时, (U2) -> S2才为(U1) -> S1的子类型

即, 函数类型之间如果要存在父子类型关系, 参数列表的每个形参之间必须存在同”方向”的父子类型关系, 且 返回值类型之间 必须存在与 参数列表父子”方向”相反的父子类型关系

即, 如果参数列表U1 <: U2, 那么 只有S2 <: S1, 两个函数类型之间才存在(U2) -> S2 <: (U1) -> S1

反之, 则是 如果参数列表U2 <: U1, 那么 只有S1 <: S2, 才存在(U1) -> S1 <: (U2) -> S2

可以发现, 函数类型之间的父子类型 跟随返回值之间的父子类型方向

总结, 函数类型之间, 只有 两参数列表存在父子类型关系, 且返回值之间存在 与 参数列表相反的父子类型关系时, 函数类型才会存在与返回值类型相同的父子类型关系

永远成立的子类型关系#

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

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

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

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

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

仓颉中存在两个最基础类型: AnyObject:

  1. Any是接口类型, 所有类型都是Any的子类型(Object除外)

  2. Objectclass类型, 所有class都是Object的子类型(Any除外)

其次, 还存在一个Nothing类型, 它是所有类型的子类型

传递性带来的子类型关系#

子类型关系具有传递性

如下代码中, 虽然只描述了I2 <: I1C <: I2以及Bool <: I2, 但根据子类型的传递性, 也隐式存在C <: I1以及Bool <: I1这两个子类型关系

interface I1 { }
interface I2 <: I1 { }
class C <: I2 { }
extend Bool <: I2 { }

仓颉子类型关系具有传递性, 比较容易理解

简单点说, 如果Type1Type2之间存在父子类型关系, 且Type2Type3之间也存在父子类型关系, 那么Type1Type3之间可能也存在父子类型关系

具体是否存在, 就看两种父子类型关系的方向了, 并不复杂

泛型类型的子类型关系#

泛型类型间也有子类型关系, 详见 泛型类型的子类型关系

泛型类型之间的父子类型关系, 具体到时候在了解吧

类型转换#

仓颉不支持不同类型之间的隐式转换(子类型天然是父类型, 所以子类型到父类型的转换不是隐式类型转换), 类型转换必须显式地进行

下面将依次介绍数值类型之间的转换, RuneUInt32和整数类型到Rune的转换, 以及isas操作符

仓颉中, 存在父子类型关系的类型 从子类型到父类型的转换不属于隐式类型转换

且, 仓颉不允许隐式类型转换, 类型转换必须显式进行

数值类型之间的转换#

对于数值类型(包括: Int8, Int16, Int32, Int64, IntNative, UInt8, UInt16, UInt32, UInt64, UIntNative, Float16, Float32, Float64), 仓颉支持使用T(e)的方式得到一个值等于e, 类型为T的值

其中, 表达式e的类型和T可以是上述任意数值类型

下面的例子展示了数值类型之间的类型转换:

main() {
let a: Int8 = 10
let b: Int16 = 20
let r1 = Int16(a)
println("The type of r1 is 'Int16', and r1 = ${r1}")
let r2 = Int8(b)
println("The type of r2 is 'Int8', and r2 = ${r2}")
let c: Float32 = 1.0
let d: Float64 = 1.123456789
let r3 = Float64(c)
println("The type of r3 is 'Float64', and r3 = ${r3}")
let r4 = Float32(d)
println("The type of r4 is 'Float32', and r4 = ${r4}")
let e: Int64 = 1024
let f: Float64 = 1024.1024
let r5 = Float64(e)
println("The type of r5 is 'Float64', and r5 = ${r5}")
let r6 = Int64(f)
println("The type of r6 is 'Int64', and r6 = ${r6}")
}

上述代码的执行结果为:

The type of r1 is 'Int16', and r1 = 10
The type of r2 is 'Int8', and r2 = 20
The type of r3 is 'Float64', and r3 = 1.000000
The type of r4 is 'Float32', and r4 = 1.123457
The type of r5 is 'Float64', and r5 = 1024.000000
The type of r6 is 'Int64', and r6 = 1024

注意:

类型转换时可能发生溢出, 若溢出可提前在编译器检测出来, 则编译器会直接给出报错, 否则根据默认的溢出策略将抛出异常

仓颉的数值类型之间是可以进行显式转换的, 比如: Int64Int32或其他整型之间, Float32Float64或其他浮点类型之间, Int64Float64之间. 实际上 所有数值类型之间都可以发生显式类型转换

但仓颉编译器对类型转换的检查比较严格, 如果类型转换操作会发生溢出, 如果编译器可以检测出来, 则直接编译报错, 否则 运行时抛异常

这段代码是示例:

main() {
let i64: Int64 = 512
let i8: Int8 = Int8(i64)
return 0
}

尝试编译这段代码将会发生编译报错:

error: integer type conversion overflow
==> /home/humid1ch/CanjieProjects/First/src/main.cj:320:20:
|
320 | let i8: Int8 = Int8(i64)
| ^^^^^^^^^ type conversion from Int64(512) to Int8 would overflow
|
# note: range of Int8 is -128 ~ 127
1 error generated, 1 error printed.
1 warning generated, 1 warning printed.
Error: failed to compile package`Main`, return code is 1
Error: cjpm run failed

即, 检测到类型转换时, 数据到目标数据会发生溢出, 直接编译报错

RuneUInt32和整数类型到Rune的转换#

RuneUInt32的转换使用UInt32(e)的方式, 其中e是一个Rune类型的表达式, UInt32(e)的结果是eUnicode scalar value对应的UInt32类型的整数值

整数类型到Rune的转换使用Rune(num)的方式, 其中num的类型可以是任意的整数类型, 且仅当num的值落在[0x0000, 0xD7FF][0xE000, 0x10FFFF](即Unicode scalar value)中时, 返回对应的Unicode scalar value表示的字符, 否则, 编译报错(编译时可确定num的值)或运行时抛异常

下面的例子展示了RuneUInt32之间的类型转换:

main() {
let x: Rune = 'a'
let y: UInt32 = 65
let r1 = UInt32(x)
let r2 = Rune(y)
println("The type of r1 is 'UInt32', and r1 = ${r1}")
println("The type of r2 is 'Rune', and r2 = ${r2}")
}

上述代码的执行结果为:

The type of r1 is 'UInt32', and r1 = 97
The type of r2 is 'Rune', and r2 = A

仓颉中, Rune类型是表示Unicode的单个字符的类型

而整型类型的数据是可以转换为部分字符, 最基础的比如ASCII字符, 此时就可以直接发生UInt32 -> Rune的类型转换了

所以, 仓颉规定 当任意整型数据范围在[0x0000(0), 0xD7FF(55295)][0xE000(57344), 0x10FFFF(1114111)] 时, 可以显式转换为Rune类型, 否则 编译报错或运行时抛异常

Rune所有数据都可以转换为UInt32类型数据, Rune不允许转换为其他整型类型, 只能转换为UInt32

isas操作符#

仓颉支持使用is操作符, 来判断 某个表达式的类型 是否是 指定的类型(或其子类型)

具体而言, 对于表达式e is T(e可以是任意表达式, T可以是任何类型), 当e的运行时类型是T的子类型时, e is T的值为true, 否则e is T的值为false

下面的例子展示了is操作符的使用:

open class Base {
var name: String = "Alice"
}
class Derived <: Base {
var age: UInt8 = 18
}
main() {
let a = 1 is Int64
println("Is the type of 1 'Int64'? ${a}")
let b = 1 is String
println("Is the type of 1 'String'? ${b}")
let b1: Base = Base()
let b2: Base = Derived()
var x = b1 is Base
println("Is the type of b1 'Base'? ${x}")
x = b1 is Derived
println("Is the type of b1 'Derived'? ${x}")
x = b2 is Base
println("Is the type of b2 'Base'? ${x}")
x = b2 is Derived
println("Is the type of b2 'Derived'? ${x}")
}

上述代码的执行结果为:

Is the type of 1 'Int64'? true
Is the type of 1 'String'? false
Is the type of b1 'Base'? true
Is the type of b1 'Derived'? false
Is the type of b2 'Base'? true
Is the type of b2 'Derived'? true

仓颉存在is操作符, 用于 对目标进行 运行时类型 判断, 语法为:

e is T
// e 为目标表达式, T 为判断类型

is可以判断 目标表达式是否为某个类型或此类型的子类型, 且 判断的是运行时类型, 而不是静态类型

此表达式的类型为Bool

as操作符可以用于 将某个表达式的类型转换为指定的类型

因为类型转换有可能会失败, 所以as操作返回的是一个Option类型

具体而言, 对于表达式e as T(e可以是任意表达式, T可以是任何类型), 当e的运行时类型是T的子类型时, e as T的值为Option<T>.Some(e), 否则e as T的值为Option<T>.None

下面的例子展示了as操作符的使用(注释中标明了as操作的结果):

open class Base {
var name: String = "Alice"
}
class Derived <: Base {
var age: UInt8 = 18
}
let a = 1 as Int64 // a = Option<Int64>.Some(1)
let b = 1 as String // b = Option<String>.None
let b1: Base = Base()
let b2: Base = Derived()
let d: Derived = Derived()
let r1 = b1 as Base // r1 = Option<Base>.Some(b1)
let r2 = b1 as Derived // r2 = Option<Derived>.None
let r3 = b2 as Base // r3 = Option<Base>.Some(b2)
let r4 = b2 as Derived // r4 = Option<Derived>.Some(b2)
let r5 = d as Base // r5 = Option<Base>.Some(d)
let r6 = d as Derived // r6 = Option<Derived>.Some(d)

仓颉也存在as操作符, 可以 尝试将任意表达式转换为任意类型, 语法为:

e as T
// e 为目标表达式, T 可以为任意类型

此表达式的类型为Option<T>类型的, 即 表达式的值 转换成功为Option<T>.Some(e), 失败则为Option<T>.None

asis一样, 也是针对运行时类型