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的子类型下文将说明两个类型为子类型关系的几种情况
仓颉是强类型语言, 不同类型之间 是不允许存在 隐式类型转换 的
不过, 某些类型之间是可以通过显式类型转换语法进行转换的, 比如Int64与Float64之间的转换
除此之外, 实际上 拥有父子类型关系的类型, 仓颉是允许隐式类型转换的
但, 只允许子类型转换为父类型, 父类型禁止转换为子类型
继承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 { }
一个类型实现了一个接口, 则此类型与接口之间默认具有父子类型关系, 接口为父类型 实现接口的类型为子类型
元组类型的子类型关系
仓颉语言中的元组类型也有子类型关系
直观的, 如果一个元组
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()) // OKlet t2: (C3, C1) = (C4(), C2()) // OK
元组类型也存在父子类型关系, 因为元组类型是基于其他数据类型的, 所以 当元组内 元素的类型均具有同”方向”的父子类型关系时, 则 元组类型之间也具有同”方向”的父子类型关系
什么意思呢?
如果存在元组类型: (Type1, Type2)和(Type3, Type4), 如果Type1 <: Type3且Type2 <: Type4, 则(Type1, Type2) <: (Type3, Type4)
元组类型之间如果想要存在父子类型关系, 需要保证 元组的所有元素类型, 按照顺序具有同”方向”的父子类型关系
函数类型的子类型关系
仓颉语言中, 函数是一等公民, 而函数类型亦有子类型关系: 给定两个函数类型
(U1) -> S2和(U2) -> S1, 如果存在(U1) -> S2是(U2) -> S1的子类型, 当且仅当U2是U1的子类型, 且S2是S1的子类型(注意顺序)例如下面的代码定义了两个函数
f : (U1) -> S2和g : (U2) -> S1, 且f的类型是g的类型的子类型由于
f的类型是g的子类型, 所以代码中使用到g的地方都可以换为fopen 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()) // Okf(U2()) // Ok}func h(lam: (U2) -> S1): S1 {lam(U2())}func call2() {h(g) // Okh(f) // Ok}对于上面的规则,
S2 <: S1部分很好理解: 函数调用产生的结果数据会被后续程序使用, 函数g可以产生S1类型的结果数据, 函数f可以产生S2类型的结果, 而g产生的结果数据应当能被f产生的结果数据替代, 因此要求S2 <: S1对于
U2 <: U1的部分, 可以这样理解: 在函数调用产生结果前, 它本身应当能够被调用, 函数调用的实参类型固定不变, 同时形参类型要求更宽松时, 依然可以被调用, 而形参类型要求更严格时可能无法被调用——例如给定上述代码中的定义g(U2())可以被换为f(U2()), 正是因为实参类型U2的要求更严格于形参类型U1
仓颉是强类型语言, 函数是拥有具体类型的, 而函数类型之间同样可以具有父子类型关系
文档中的例子描述的有一些混乱
如果存在两个函数类型: (U1) -> S1和(U2) -> S2
只有在, U1为U2的子类型, 且S2为S1的子类型时, (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
仓颉中存在两个最基础类型: Any和Object:
-
Any是接口类型, 所有类型都是Any的子类型(Object除外) -
Object是class类型, 所有class都是Object的子类型(Any除外)
其次, 还存在一个Nothing类型, 它是所有类型的子类型
传递性带来的子类型关系
子类型关系具有传递性
如下代码中, 虽然只描述了
I2 <: I1、C <: I2以及Bool <: I2, 但根据子类型的传递性, 也隐式存在C <: I1以及Bool <: I1这两个子类型关系interface I1 { }interface I2 <: I1 { }class C <: I2 { }extend Bool <: I2 { }
仓颉子类型关系具有传递性, 比较容易理解
简单点说, 如果Type1与Type2之间存在父子类型关系, 且Type2与Type3之间也存在父子类型关系, 那么Type1和Type3之间可能也存在父子类型关系
具体是否存在, 就看两种父子类型关系的方向了, 并不复杂
泛型类型的子类型关系
泛型类型间也有子类型关系, 详见 泛型类型的子类型关系
泛型类型之间的父子类型关系, 具体到时候在了解吧
类型转换
仓颉不支持不同类型之间的隐式转换(子类型天然是父类型, 所以子类型到父类型的转换不是隐式类型转换), 类型转换必须显式地进行
下面将依次介绍数值类型之间的转换,
Rune到UInt32和整数类型到Rune的转换, 以及is和as操作符
仓颉中, 存在父子类型关系的类型 从子类型到父类型的转换不属于隐式类型转换
且, 仓颉不允许隐式类型转换, 类型转换必须显式进行
数值类型之间的转换
对于数值类型(包括:
Int8,Int16,Int32,Int64,IntNative,UInt8,UInt16,UInt32,UInt64,UIntNative,Float16,Float32,Float64), 仓颉支持使用T(e)的方式得到一个值等于e, 类型为T的值其中, 表达式
e的类型和T可以是上述任意数值类型下面的例子展示了数值类型之间的类型转换:
main() {let a: Int8 = 10let b: Int16 = 20let 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.0let d: Float64 = 1.123456789let 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 = 1024let f: Float64 = 1024.1024let 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 = 10The type of r2 is 'Int8', and r2 = 20The type of r3 is 'Float64', and r3 = 1.000000The type of r4 is 'Float32', and r4 = 1.123457The type of r5 is 'Float64', and r5 = 1024.000000The type of r6 is 'Int64', and r6 = 1024注意:
类型转换时可能发生溢出, 若溢出可提前在编译器检测出来, 则编译器会直接给出报错, 否则根据默认的溢出策略将抛出异常
仓颉的数值类型之间是可以进行显式转换的, 比如: Int64与Int32或其他整型之间, Float32与Float64或其他浮点类型之间, Int64与Float64之间. 实际上 所有数值类型之间都可以发生显式类型转换
但仓颉编译器对类型转换的检查比较严格, 如果类型转换操作会发生溢出, 如果编译器可以检测出来, 则直接编译报错, 否则 运行时抛异常
这段代码是示例:
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 1Error: cjpm run failed即, 检测到类型转换时, 数据到目标数据会发生溢出, 直接编译报错
Rune到UInt32和整数类型到Rune的转换
Rune到UInt32的转换使用UInt32(e)的方式, 其中e是一个Rune类型的表达式,UInt32(e)的结果是e的Unicode scalar value对应的UInt32类型的整数值整数类型到
Rune的转换使用Rune(num)的方式, 其中num的类型可以是任意的整数类型, 且仅当num的值落在[0x0000, 0xD7FF]或[0xE000, 0x10FFFF](即Unicode scalar value)中时, 返回对应的Unicode scalar value表示的字符, 否则, 编译报错(编译时可确定num的值)或运行时抛异常下面的例子展示了
Rune和UInt32之间的类型转换:main() {let x: Rune = 'a'let y: UInt32 = 65let 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 = 97The 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
is和as操作符
仓颉支持使用
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 Int64println("Is the type of 1 'Int64'? ${a}")let b = 1 is Stringprintln("Is the type of 1 'String'? ${b}")let b1: Base = Base()let b2: Base = Derived()var x = b1 is Baseprintln("Is the type of b1 'Base'? ${x}")x = b1 is Derivedprintln("Is the type of b1 'Derived'? ${x}")x = b2 is Baseprintln("Is the type of b2 'Base'? ${x}")x = b2 is Derivedprintln("Is the type of b2 'Derived'? ${x}")}上述代码的执行结果为:
Is the type of 1 'Int64'? trueIs the type of 1 'String'? falseIs the type of b1 'Base'? trueIs the type of b1 'Derived'? falseIs the type of b2 'Base'? trueIs 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>.Nonelet 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>.Nonelet 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
as与is一样, 也是针对运行时类型