NOTE阅读文档版本:
语言规约 Cangjie-0.53.18-Spec
具体开发指南 Cangjie-LTS-1.0.3
在阅读 了解仓颉的语言规约时, 难免会涉及到一些仓颉的示例代码, 但 我们对仓颉并不熟悉, 所以可以用 仓颉在线体验 快速验证
有条件当然可以直接 配置Canjie-SDK
WARNING博主在此之前, 基本只接触过C/C++语言, 对大多现代语言都没有了解, 所以在阅读过程中遇到相似的概念, 难免会与C/C++中的相似概念作类比, 见谅
此样式内容, 表示文档原文内容
类型
仓颉编程语言是一种 静态类型(
statically typed) 语言: 大部分保证程序安全的类型检查发生在编译期同时, 仓颉编程语言是一种 强类型(
strongly typed) 语言: 每个表达式都有类型, 并且表达式的类型决定了它的取值空间和它支持的操作静态类型和强类型机制可以帮助程序员在编译阶段发现大量由类型引发的程序错误
类型转换
作为一种强类型(
strongly typed)语言, 仓颉编程语言仅支持显式类型转换(亦称强制类型转换)对于值类型, 支持使用
valueType(expr)实现将表达式expr的类型转换成valueType对于
class和interface, 通过使用as操作符实现静态类型转换对于值类型, 我们说
valueTypeA到valueTypeB是可转换的, 是指定义了将valueTypeA转换成valueTypeB的转换规则, 对于没有定义转换规则的两个值类型, 我们称它们是不可转换的对于
class和interface, 如果两个类型在继承关系图上存在父子类型关系, 那么这两个类型之间就是可转换的(当然, 转换的结果也有可能是失败的), 否则, 这两个类型之间就是不可转换的对于其它未提及的类型, 仓颉不支持通过上述两种方式实现它们之间的类型转换
仓颉作为强类型语言, 只支持强制类型转换:
-
值类型, 支持
Type(expr)强制类型转换与C语言的强制类型转换类似
-
对于
class和interface, 则支持通过as操作符, 静态类型转换
Value Type之间的类型转换
对于数值类型, 支持如下类型转换**(未列出即表示不支持)**:
Rune类型到UInt32类型的转换- 整数类型(包括
Int8,Int16,Int32,Int64,IntNative,UInt8,UInt16,UInt32,UInt64,UIntNative)到Rune类型的转换- 所有数值类型(包括
Int8,Int16,Int32,Int64,IntNative,UInt8,UInt16,UInt32,UInt64,UIntNative,Float16,Float32,Float64)之间的双向转换
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的值)或运行时抛异常main(){var c: Rune = 'a'var num: UInt32 = 0num = UInt32(c) // num = 97num -= 32 // num = 65c = Rune(num) // c =`A`return 0}
仓颉值类型的强制类型转换, 只支持:
- 整型和浮点型之间的转换
Rune到UInt32的转换- 整型到
Rune有规则限制的转换
如果把Rune单纯看作C/C++中的char, 那与C中的整型与浮点型之间的强制类型转换还是比较相似的
但Rune可表示字符的范围, 要比C/C++中的char要大多了, 所以整型到Rune的转换才要限制
为了保证类型安全, 仓颉编程语言不支持数值类型之间的隐式类型转换(数值字面量的类型由上下文推断得到, 这种情形并不是隐式类型转换), 要实现一种数值类型到另外一种数值类型的转换, 必须使用显式的方式:
NumericType(expr), 表示将expr的类型强制转换为NumericType类型(NumericType表示任意一种数值类型), 如转换成功, 会返回一个新的从expr构造而来的类型为NumericType的值数值类型转换的语法定义为:
numericTypeConvExpr: numericTypes '(' expression ')';numericTypes: 'Int8' | 'Int16' | 'Int32' | 'Int64' | 'IntNative' | 'UInt8' | 'UInt16' | 'UInt32' | 'UInt64' | 'UIntNative' | 'Float16' | 'Float32' | 'Float64';
如果根据数值类型所占
bit数来定义数值类型间的”大小关系”(所占bit数越多, 类型越”大”, 所占bit数越少, 类型越”小”), 则仓颉编程语言支持以下类型转换:a)有符号整数类型之间的双向转换: 小转大时数值结果不变, 大转小时 若超出小类型的表示范围, 则根据上下文中的属性宏确定溢出处理策略(默认使用抛出异常的策略), 不同溢出策略详见[算术表达式]
下面以
Int8和Int16之间的转换为例进行说明(溢出时, 使用抛异常的处理策略):main(){var i8Number: Int8 = 127var i16Number: Int16 = 0i16Number = Int16(i8Number) // ok: i16Number = 127i8Number = Int8(i16Number) // ok: i8Number = 127i16Number = 128i8Number = Int8(i16Number) // throw an ArithmeticExceptionreturn 0}b)无符号整数类型之间的双向转换: 规则同上
以
UInt16和UInt32之间的转换为例进行说明(其他情况遵循一样的规则):main(){var u16Number: UInt16 = 65535var u32Number: UInt32 = 0u32Number = UInt32(u16Number) // ok: u32Number = 65535u16Number = UInt16(u32Number) // ok: u16Number = 65535u32Number = 65536u16Number = UInt16(u32Number) // throw an ArithmeticExceptionreturn 0}
仓颉这一点就与C/C++很不一样!
同符号的整型整型之间的转换, 溢出默认抛异常!!!!
我勒个强类型语言啊, C/C++如果这样就能少很多不容易定位的BUG了
c)浮点类型之间的双向转换: 使用
round-to-nearest模式举例说明
Float32和Float64之间的转换:main(){var f32Number: Float32 = 1.1var f64Number: Float64 = 0.0f64Number = Float64(f32Number) // f64Number = 1.100000023841858f32Number = Float32(f64Number) // f32Number = 1.1f64Number = 1.123456789f32Number = Float32(f64Number) // f32Number = 1.1234568f32Number = 4.4E38 // f32Number = POSITIVE_INFINITYf64Number = Float64(f32Number) // f64Number = POSITIVE_INFINITYf64Number = 4.4E38f32Number = Float32(f64Number) // f32Number = POSITIVE_INFINITYf64Number = Float64(f32Number * 0.0)f32Number = Float32(f64Number) // f32Number = NaNreturn 0}
从示例来看, round-to-nearest好像是 就近原则?
查了一下资料: 四舍六入五取偶: 溢出位<=4舍去, >=6进位, ==5看前一位的奇偶, 偶就进位, 奇就舍去
- 高精度转低精度, 如果超出范围, 会变成无穷
- 无穷只能转成无穷
NaN也只能转成NaN
d)有符号整数类型和无符号整数类型之间的双向转换:
因为任何有符号整数类型的表示范围 均不能 包含长度相同的无符号整数类型的表示范围(反之亦然), 因此它们之间进行转换时, 只要 待转换表达式的值落在目标整数类型的表示范围之内则转换成功, 否则根据上下文中的属性宏确定溢出处理策略(默认使用抛出异常的策略), 不同溢出策略详见[算术表达式]
下面以
Int8和UInt8之间的转换为例进行说明(溢出时, 使用抛异常的处理策略):main(){var i8Number: Int8 = 127var u8Number: UInt8 = 0u8Number = UInt8(i8Number) // ok: u8Number = 127u8Number = 100i8Number = Int8(u8Number) // ok: i8Number= 100i8Number= -100u8Number = UInt8(i8Number) // throw an ArithmeticExceptionu8Number = 255i8Number = Int8(u8Number) // throw an ArithmeticExceptionreturn 0}
有符号和无符号整型之间转换, 如果没有溢出, 就获得最终值
如果存在溢出, 直接抛异常
e)整数转换为浮点数: 结果为尽可能接近原整数的浮点数
超出目标类型的表示范围时, 返回
POSITIVE_INFINITY或NEGTIVE_INFINITYf)浮点数转换为整数: 浮点类型到有符号整数类型的转换使用
round-toward-zero模式, 即保留整数部分舍弃小数部分当整数部分超出目标整数类型的表示范围, 则根据上下文中的整数溢出策略处理
如果是
throwing策略, 那么抛出异常;否则按如下规则转换:
NaN返回0- 小于整数取值范围下界时(包括负无穷), 返回整数的取值范围下界
- 大于整数取值范围上界时(包括正无穷), 返回整数的取值范围上界
main(){var i32Number: Int32 = 1024var f16Number: Float16 = 0.0var f32Number: Float32 = 0.0f16Number = Float16(i32Number) // ok: f16Number = 1024.0f32Number = Float32(i32Number) // ok: f32Number = 1024.0i32Number = 2147483647f16Number = Float16(i32Number) // f16Number = POSITIVE_INFINITY 正无穷f32Number = Float32(i32Number) // precision lost: f32Number = 2.14748365E9 精度丢失f32Number = 1024.1024i32Number = Int32(f32Number) // ok: i32Number = 1024f32Number = 1024e10i32Number = Int32(f32Number) // throw an Exceptionf32Number = 3.4e40 // f32Number = POSITIVE_INFINITYi32Number = Int32(f32Number) // throw an Exceptionf32Number = 3.4e40 * 0.0 // f32Number = NaNi32Number = Int32(f32Number) // throw an Exceptionreturn 0}
从示例来看:
-
整数->浮点数
如果超出了浮点数可表示的最大范围, 浮点数会变为无穷
如果没有超出范围, 但是超出了精度范围, 就会丢失一定的精度
-
浮点数->整数
如果整数部分没有超出有效范围, 就取整
如果整数部分超出有效范围, 就是溢出, 会抛异常
如果浮点数是
NaN或无穷, 也会抛异常
class/interface之间的类型转换
对于一个
class/interface类型的实例obj, 如果需要将它的(静态)类型转换到另一个class/interface类型TargetType, 可使用:obj as TargetType关于
as操作符的使用, 以及class/interface之间的类型转换规则, 参见[as 操作符]
虽然没有介绍, 但是推测应该与继承关系 有关系
类型别名
当某个类型的名字比较复杂或者在特定场景中不够直观时, 可以选择使用类型别名的方式为此类型取一个简单并且直观的别名. 定义类型别名的语法为:
typeAlias: typeModifier?`type`identifier typeParameters?`=`type;其中,
typeModifier是可选的可访问性修饰符(即public),type是关键字,identifier是任意的合法标识符,type是任意的在top-level可见的类型,identifier和type之间使用=进行连接另外, 也可通过在
identifier之后添加类型参数(typeParameters)的方式定义泛型别名通过以上声明, 即为类型
type定义了一个名字为identifier的别名, 并且identifier和type被视作同一种类型例如:
type Point2D = (Float64, Float64)type Point3D = (Float64, Float64, Float64)let point1: Point2D = (0.5, 0.8)let point2: Point3D = (0.5, 0.8, 1.1)上述
type定义并不会定义一个新的类型, 它的作用仅仅是为某个已有类型定义另外一个名字而已, 别名和原类型被视作同一个类型, 并且别名不会对原类型的使用带来任何影响
仓颉中的type关键字, 应该和C/C++中的typedef类似, 与C++中的using的一部分功能也类似
使用比较简单:
type 别名(<泛型类型参数>)= 实际类型(可以是泛型)类型别名定义的规则
类型别名的定义只能出现在
top-levelfunc test(){type Point2D = (Float64, Float64) // error: type alias can only be defined at top-leveltype Point3D = (Float64, Float64, Float64) // error: type alias can only be defined at top-level}用
type定义类型别名时, 原类型必须在type定义的位置可见class LongNameClassA {}type ClassB = LongNameClassB // error: use of undeclared type 'LongNameClassB'定义泛型别名时, 如果泛型别名中引入了原类型中没有使用的泛型参数, 则编译器会告警
type Class1<V> = GenericClassA<Int64, V> // ok. ClassA is a generic classtype Class2<Value, V> = GenericClassB<Int64, V> // warning: the type parameter 'Value' in 'Class2<Value, V>' is not used in 'GenericClassB<Int64, V>'type Int<T> = Int32 // warning: the type parameter 'T' in 'Int<T>' is not used in`Int32`
仓颉中的type与C语言中的typedef有不同点
type只能用在顶层作用域, 不能出现在{}代码块中
而C/C++中的typedef可以出现在任何地方
定义泛型别名时, 不允许为别名和原类型中的类型参数添加泛型约束, 在用到泛型别名时, 可以按需为其添加泛型约束
另外, 原类型中已有的泛型约束会”传递”至别名
type Class1<V> where V <: MyTrait = GenericClassA<Int64, V> // error: generic constraints are not allowed heretype Class2<V> = GenericClassB<Int64, V> where V <: MyTrait // error: generic constraints are not allowed heretype Class3<V> = GenericClassC<Int64, V>func foo<V> (p: Class3<V>)where V <: MyTrait { // add generic constraints when 'Class3<V>' is usedfunctionBody}class ClassWithLongName<T> where T<:MyTrait {classBody}type Class<T> = ClassWithLongName<T> // Class<T> also has the constraint 'where T<:MyTrait'
别名只是起别名, 定义别名 不能额外添加原类型不存在的功能
但是使用时, 与原类型保持一致
一个(或多个)
type定义中禁止出现循环引用(无论是直接的或是间接的)其中, 判断循环引用的方式是通过名字判断是否存在循环引用, 并不是使用类型展开的方式
type Value = GenericClassAp<Int64, Value> // error: 'Value' references itselftype Type1 = (Int64)->Type1 // error: 'Type1' references itselftype Type2 = (Int64, Type2) // error: 'Type2' references itselftype Type3 = Type4 // error: 'Type3' indirectlly references itselftype Type4 = Type3
类型别名被视为与原类型等价的类型
例如, 在下面的例子中, 可以将
Int类型的参数和Int32类型的参数直接相加(Int定义为Int32的别名)注意, 不能通过使用别名达到函数重载的目的:
type Int = Int32let numOne: Int32 = 10let numTwo: Int = 20let numThree = numOne + numTwofunc add(left: Int, right: Int32): Int { left + right }func add(left: Int32, right: Int32): Int32 { left + right } // error: invalid redeclaration of 'add : (Int32, Int32)->Int32'
type定义的默认可见性为default如果需要 在其他
package内使用本package中定义的类型别名, 需要同时满足:(1)原类型在本
package中的可见性修饰符为public(2)
type定义使用public修饰符另外, 需要注意的是: 别名可以与原类型拥有不同的可见范围, 但是别名的的可见范围不能大于原类型的可见范围
a.cj package Apublic class ClassWithLongNameA {}class ClassWithLongNameB {}public type classA = ClassWithLongNameA // oktype classAInter = ClassWithLongNameA // ok/* error: classB can not be declared with modifier 'public', as 'ClassWithLongNameB' is internal */public type classB = ClassWithLongNameB// b.cjpackage Bimport A.*let myClassA: A.classA = ClassWithLongNameA()
仓颉中type定义类型别名, 可以用可见性修饰符进行修饰
类型别名的使用
类型别名可以用在任何等号右手边它指向的原类型能够使用的位置:
作为类型使用, 例如:
type A = Bclass B {}var a: A = B()// Use typealias A as type B当类型别名实际指向的类型为
class、struct时, 可以作为构造器名称使用type A = Bclass B {}func foo(){ A()} // Use type alias A as constructor of B当类型别名实际指向的类型为
class、interface、struct时, 可以作为访问内部静态成员变量或函数的类型名type A = Bclass B {static var b : Int32 = 0;static func foo(){}}func foo(){A.foo()// Use A to access static method in class BA.b}当类型别名实际指向的类型为
enum时, 可以作为enum声明的构造器的类型名enum TimeUnit {Day | Month | Year}type Time = TimeUnitvar a = Time.Dayvar b = Time.Month // Use type alias Time to access constructors in TimeUnit
类型间的关系
类型间的关系有两种: 相等和子类型
C/C++中, 只有存在继承关系的类之间又一些关系
看来仓颉的类型之间存在许多的关系
类型相等
对于任意两个类型
T1和T2, 如果它们满足以下任一条件, 则称T1和T2相等(记为T1 === T2):
- 存在类型别名定义
type T1 = T2;- 在
class定义的内部和class的extend内部,T1是class的名字,T2是This;T1和T2的名字完全相同(自反性);T2 === T1(对称性);- 存在类型
Tk, 满足T1 === Tk且Tk === T2(传递性);
上面的所有情况的结论都是, T1 === T2, 也就是说每条之后都要加一句T1 === T2
第一眼只看每条句子, 一下子没看懂
子类型
对于任意两个类型
T1和T2, 如果它们满足以下任一条件, 则称T1是T2的子类型(记为T1 <: T2):
T1 === T2;T1是Nothing类型;T1和T2均是Tuple类型, 并且T1每个位置处的类型都是T2对应位置处类型的子类型;T1和T2均是Function类型, 并且T2的参数类型是T1参数类型的子类型,T1的返回类型是T2返回类型的子类型;T1是任意class类型,T2是Object类型;T1和T2均是interface类型, 并且T1继承了T2;T1和T2均是class类型, 并且T1继承了T2;T2是interface类型, 并且T1实现了T2;- 存在类型
Tk, 满足T1 <: Tk且Tk <: T2(传递性)
只从说明可以看出, 仓颉中:
-
Nothing是所有类型的子类型 -
Object是所有class的子类型 -
存在继承关系的
class和interface类型, 被继承的类型(C++中的基类)是父类型, 子类是子类型即, 如果
T1继承了T2,T1属于子类型 -
如果两个
interface,T1实现了T2,T1是T2的子类型可以看作是
T2生出了T1
最小公共父类型
在有子类型的类型系统里, 有时会遇到需要求两个类型的最小公共父类型的情形, 例如
if表达式的类型便是其两个分支的类型的最小公共父类型,match表达式类似两个类型的最小公共父类型, 是其公共父类型中最小的一个
最小意味着它是其他所有公共父类型的子类型
最小公共父类型定义如下:
对于任意两个类型
T1和T2, 如果类型LUB满足如下规则, 则LUB是T1和T2的最小公共父类型:
- 对于同时满足
T1 <: T和T2 <: T的任意类型T,LUB <: T也成立注意, 如果公共父类型中的某个类型不比其他类型大, 它只是极小的, 并不一定是最小的
参考数学中的集合
最大公共子类型
因为子类型关系中存在逆变(定义参考泛型章节下的类型型变)的情形, 如函数类型的参数类型是逆变的, 此时会需要求两个类型的最大公共子类型
两个类型的最大公共子类型, 是其公共子类型中最大的一个
最大意味着它是其他所有公共子类型的父类型
最大公共子类型定义如下: 对于任意两个类型
T1和T2, 如果类型GLB满足如下规则, 则GLB是T1和T2的最大公共子类型:
- 对于同时满足
T <: T1和T <: T2的任意类型T,T <: GLB也成立注意, 如果公共子类型中的某个类型不比其他类型小, 它只是极大的, 并不一定是最大的
同样参考数学中的集合
类型安全
在没有数据竞争的情况下, 编译器保证内存安全和类型安全
下面是一个类型安全和内存安全都 得不到保证 的例子:
class C {var x = 1var y = 2var z = 3}enum E {A(Int64)| B(C)}var e = A(1)main(){spawn {while (true){e = B(C()) // writing to`e`e = A(0)}}while (true){match (e){ // reading from`e`case A(n)=>println(n+1)case B(c)=>c.x = 2c.x = 3c.x = 4}}}不保证是因为 对变量
e赋值的线程和读取该变量的线程之间存在数据竞争有关数据竞争的更多信息, 请参见第 15 章 “并发”