仓颉编程语言中用于存储数值的数据类型有 整数类型 和 浮点类型 。其中,整数类型用于存储整数,浮点类型用于存储具有小数部分的实数。
仓颉的整数类型分为两种:有符号(signed)整数类型和无符号(unsigned)整数类型,如表3-1所示。
表3-1 仓颉的整数类型
有符号整数类型包括5种:Int8、Int16、Int32、Int64和IntNative。所有类型名称均以Int开头,后面所接的数字表示存储一个该类型的数据所需要的二进制位数。IntNative表示存储时需要的二进制位数与平台相关。同样,无符号整数类型也包括5种,不过所有类型名称均以UInt开头。
“Int”是单词“integer”的前3个字符,integer的意思是“整数”。UInt中的“U”是单词“unsigned”的第1个字符,表示“无符号的”。
由表3-1可知,不同的整数类型能够表示的整数范围是不同的。例如,从理论上说,8位二进制应该能够表示2 8 (256)个不同的整数,而同样是使用8位二进制存储整数,Int8类型的表示范围为−128~127,UInt8类型的表示范围为0~255。这是因为,对于一个Int8类型的有符号整数,其最高位是符号位,表示正负号,真正用于表示数值大小的只有7位,所以其表示范围为−2 7 ~−1、0、1~2 7 −1。对于一个UInt8类型的无符号整数,由于没有符号位,因此其表示范围为0、1~2 8 −1。
在给整数类型变量赋值时,要避免发生 溢出 错误。举例如下:
main() {
var x: Int8
x = 128 // 溢出错误:128超出了Int8类型的表示范围
}
在以上示例中,由于x是Int8类型的变量,而Int8类型的表示范围为128~127,因此,将超出Int8类型表示范围的字面量128赋给x时溢出了。
在使用整数类型字面量时,可以给字面量后面加上类型后缀,以明确整数字面量的类型。整数类型与类型后缀的对应关系如表3-2所示。
表3-2 整数类型与类型后缀的对应关系
以下是一些使用了类型后缀的整数类型字面量:
18i8 // Int8类型的字面量18
2023u16 // UInt16类型的字面量2023
30000i32 // Int32类型的字面量30000
为了提高整数类型字面量的可读性,可以使用下画线(_)作为分隔符。例如,字面量10000000000可以写作10_000_000_000,也可以写作100_0000_0000。
自增操作符(++)用于将操作数的值加1,自减操作符(--)用于将操作数的值减1。这两个操作符只能用于 整数类型可变变量 。
自增和自减操作符都是 一元后缀 操作符。 一元 表示该操作符只需要1个操作数, 后缀 表示该操作符要放在操作数的后面。
自增和自减操作符的用法示例如下:
main() {
var i = 0
var j = 10
i++ // 使用自增操作符使i的值加1
j-- // 使用自减操作符使j的值减1
println(i) // 输出:1
println(j) // 输出:9
}
定义一个整数类型的可变变量,分别使用自增操作符和自减操作符改变该变量的值。
仓颉浮点类型有3种:Float16(半精度浮点类型)、Float32(单精度浮点类型)和Float64(双精度浮点类型),分别表示在存储浮点数时使用的二进制位数为16位、32位和64位。这3种浮点类型的精度从高到低依次为:Float64>Float32>Float16。
01 from std import format.*
02
03 main() {
04 let x: Float16 = 0.7438734374037310774
05 let y: Float32 = 0.7438734374037310774
06 let z: Float64 = 0.7438734374037310774
07
08 println(x.format(".18")) // 输出:0.743652343750000000
09 println(y.format(".18")) // 输出:0.743873417377471924
10 println(z.format(".18")) // 输出:0.743873437403731130
11 }
在代码清单3-1中,我们将一个小数位数较多的浮点类型字面量分别存入3个不同精度类型的浮点类型变量(Float16类型的x、Float32类型的y和Float64类型的z),然后读取变量值。在读取变量值时,为了观察3个变量的精度,使用了format函数将输出精度指定为小数点后18位。通过示例程序,可以发现不同浮点类型的精度是不同的:Float16的精度约为小数点后3位,Float32的精度约为小数点后7位,Float64的精度约为小数点后15位。
一个 不带指数部分 的浮点类型字面量的形式如下:
[整数部分].小数部分
以下是一些合法的浮点类型字面量:
1.234 // 同时包含整数部分和小数部分
.5 // 只包含小数部分,不包含整数部分
另外,浮点类型字面量可以使用 科学记数法 表示。在浮点类型的科学记数法中,指数部分以“e”或“E”为前缀,底数为10。对于 带指数部分 的浮点类型字面量,至少要包含整数部分或小数部分,如果不包含小数部分,那么可以省略小数点。
以下是一些以科学记数法表示的合法的浮点类型字面量:
2E3 // 表示2×103,即2000.0
.6e2 // 表示0.6×102,即60.0
9.8e-3 // 表示9.8×10-3,即0.0098
浮点类型字面量也可以使用下画线(_)作为分隔符。
在使用十进制浮点类型字面量时,也可以给字面量后面加上类型后缀。十进制浮点类型与类型后缀的对应关系如表3-3所示。
表3-3 十进制浮点类型与类型后缀的对应关系
以下是一些使用了类型后缀的十进制浮点类型字面量:
99.8f64 // Float64类型的字面量99.8
0.35f16 // Float16类型的字面量0.35
1.21e3f32 // Float32类型的字面量1210.0
分别将同一个小数位数较多的浮点类型字面量赋给不同浮点类型的变量,通过函数format和println观察不同浮点类型的精度。
对各种数值类型的数据可以进行各种算术运算,这主要依赖于 算术操作符 。仓颉算术操作符包括1个一元前缀操作符“(负号)”和6个二元操作符,如表3-4所示。
表3-4 算术操作符
由数值类型操作数和算术操作符构成的表达式,也被称为 算术表达式 。
仓颉的部分算术操作符与数学运算的规则是完全一致的,如“−(负号)”表示负数,“*”用于求积,“+”用于求和,“−(减法)”用于求差。另外,仓颉中也有一些需要注意的地方。
首先是幂运算。由于代码编辑窗口存在输入限制,不能使用上标的方式来表示幂运算,因此仓颉使用操作符“**”来表示乘方运算。举例如下:
3 ** 2 // 相当于32,计算结果为9
16.0 ** 0.5 // 相当于16.00.5,计算结果为4.0
然后是与除运算有关的两个操作符:除法(/)和取模(%)。
“/”用于求商。如果“/”的两个操作数均是浮点类型,那么运算结果也是浮点类型;如果两个操作数均是整数类型,那么运算结果也是整数类型(只保留商的整数部分)。举例如下:
16.0 / 8.0 // 结果为2.0
16.0 / 10.0 // 结果为1.6
16 / 8 // 结果为2
16 / 10 // 结果为1
“%”用于求余数,该操作符 只适用于整数类型的操作数 。余数是指除法中未被除尽的部分,余数的取值范围为0到除数之间(不包括除数本身)的数。举例如下:
16 % 8 // 结果为0
16 % 10 // 结果为6
8 % 16 // 结果为8
如果算术表达式中出现了1个以上的算术操作符,那么在运算时需要按照 操作符的优先级和结合性 来进行:优先级高的操作符先运算,优先级低的操作符后运算,相同优先级的操作符按照结合性进行运算。
仓颉的算术操作符优先级从高到低依次为:负号>乘方>乘除(乘法、除法、取模)>加减(加法、减法)。举例如下:
-6 ** 2
对于以上算术表达式,由于“−(负号)”的优先级高于“**”,因此先进行负号运算,再进行乘方运算,结果为36。
2 * 7 - 9 / 3
对于以上算术表达式,由于“*”和“/”的优先级相同,并且高于“−(减法)”,因此先进行乘法运算,再进行除法运算,最后进行减法运算,结果为11。
操作符的 结合性 指的是操作符和操作数结合的方式,分为 左结合 (从左到右结合)和 右结合 (从右到左结合)。本节介绍的各种算术操作符中,负号(−)和乘方(**)是右结合的,其他算术操作符都是左结合的。
对于以下算术表达式:
8 % 6 * 3
由于“%”和“*”的优先级相同,因此按照结合性(两者都是左结合,即从左至右的顺序)进行运算,先进行取模运算,再进行乘法运算,结果为6。
以上是一个左结合的例子,再看一个右结合的例子。对于以下算术表达式:
2.0 ** 9.0 ** 0.5
其中包含了2个右结合的操作符。在运算时,先计算9.0 ** 0.5,得到结果3.0,再计算2.0 ** 3.0,最终结果为8.0。
如果有需要,可以使用圆括号“()”来提升运算的优先级。当一个表达式中出现了多对圆括号时,按照先内层后外层、层次相同时从左至右的顺序进行运算。举例如下:
42 / ((5 - 2) * 7)
在以上算术表达式中,有内外两层圆括号。在运算时,先进行减法运算,再进行乘法运算,最后进行除法运算,结果为2。
(9 + 13) % (7 - 4)
在以上算术表达式中,有两对层次相同的圆括号。在运算时,先进行左边的加法运算,再进行右边的减法运算,最后进行取模运算,结果为1。
除了可以用于提升优先级,也可以在复杂的表达式中加上一些圆括号来提高程序的可读性。例如,可以在以上示例的算术表达式中加上一些圆括号:
(-6) ** 2
(2 * 7) - (9 / 3)
计算以下算术表达式的值,并编写程序验证:
2 ** 3
1.21 ** 0.5
3 / 2
5 / 2
3.0 / 2.0
5.0 / 2.0
3 % 2
13 % 5
3 * -7
120 / 5 % 3 * 7
120 / 5 % (3 * 7)
120 / (5 % 3) * 7
120 / (5 % (3 * 7))
在对仓颉数值类型的数据进行算术运算时,关于数据类型,有以下5点需要注意。
对于一元算术操作符“(负号)”,仓颉对其操作数的类型没有要求,只要其操作数为数值类型即可。
对于以下算术表达式:
操作数A 二元算术操作符 操作数B
如果式中的二元算术操作符不是“**”,那么仓颉要求 操作数A和操作数B必须是相同的数值类型 ,否则会引发编译错误。
“%”要求操作数A和操作数B必须是相同的整数类型。
举例如下:
main() {
let intA: Int8 = 8
let intB: Int16 = 10
println(intA * 4i8) // Int8类型和Int8类型的运算
println(intB + 4i8) // 编译错误:无法对Int16和Int8类型使用二元操作符“+”
}
如果式中的二元算术操作符是“**”,那么操作数A和操作数B的类型必须符合表3-5中所示的3种情况中的一种,否则会引发编译错误。
表3-5 乘方运算的操作数类型
举例如下:
main() {
let intC: Int64 = 3
println(intC ** 2u64) // 操作数A为Int64类型,操作数B为UInt64类型
println(intC ** 2i64) // 编译错误:无法对Int64和Int64类型使用二元操作符“**”
}
以上算术操作符对操作数类型的要求,均是在没有操作符重载的前提下。关于操作符重载的相关知识参见第10章。
对于以下算术表达式:
-操作数A
以及以下算术表达式:
操作数A 二元算术操作符 操作数B
运算结果的数据类型均 与操作数A 保持一致。假设有两个Int64类型的变量x和y,以下表达式的运算结果也是Int64类型:
-x
x + y
x * y
x ** 2u64
在进行算术运算时,必须要注意各种数值类型的表示范围,避免 溢出错误 。举例如下:
3i8 + 125i8 // 溢出
在以上算术表达式中,Int8类型的3加上Int8类型的125,其结果应该为128,但是由于Int8类型的表示范围为128~127,因此结果溢出了。
前面介绍过,在没有类型上下文的情况下,整数类型字面量会被推断为Int64类型,浮点类型字面量会被推断为Float64类型。举例如下:
println(3) // 字面量3被推断为Int64类型
println(9.5) // 字面量9.5被推断为Float64类型
如果有类型上下文,那么编译器会自动根据类型上下文来推断数值类型字面量的类型。举例如下:
main() {
let x: Float32 = 9.98
let y = x + 1.5
}
在这段代码中,x是Float32类型,因此字面量9.98被推断为Float32类型。在表达式x + 1.5中,由于加法运算要求两个操作数的类型是一致的,而x为Float32类型,因此字面量1.5也被推断为Float32类型。
对于缺省了数据类型的变量声明,编译器也会自动推断变量的数据类型。例如,在上面的代码中,变量y在声明时缺省了数据类型,而其初始值x + 1.5的类型为Float32,因此y将被推断为Float32类型。再举一个例子:
let z = 0.99
在以上代码中,由于缺少类型上下文,字面量0.99将被推断为Float64类型,因此根据初始值的类型,在声明时缺省了数据类型的变量z将会被推断为Float64类型。
仓颉对算术运算的操作数类型的要求十分严格,除“**”运算外,不同类型的操作数之间不能进行算术运算。
如果在某些情况下确实需要对不同类型的操作数进行运算,可以考虑对操作数的类型进行转换。举例如下:
main() {
var mile: Int64 = 3 // 表示英里
var kilometer: Float64 // 表示公里
kilometer = 1.609344 * mile // 编译错误
}
以上示例程序的作用是将英里换算成公里(1英里 = 1.609344公里)。由于mile和kilometer的类型不一致,因此不能直接进行乘法运算。此时,可以将表示英里的变量值转换为Float64类型,再进行乘法运算。
仓颉不支持不同类型之间的隐式转换,类型转换必须显式地进行 。仓颉支持使用以下方式得到一个值为e的T类型的 实例 。
T(e)
其中,T可以是各种数值类型,如Int8、Int16、Float32等,e可以是一个数值类型的表达式。
当我们说e是T类型的实例时,表示e是一个T类型的值。
举例如下:
Int64(9i8) // 结果为Int64类型的9
Float32(200u16) // 结果为Float32类型的200.0
回到换算英里和公里的例子,我们可以使用如下代码计算出英里对应的公里数:
kilometer = 1.609344 * Float64(mile)
在各种数值类型之间进行类型转换时,需要遵循一定的转换规则。
首先,在整数类型之间进行转换时,要确保待转换的整数必须要在目标整数类型的表示范围之内。举例如下:
Int64(200u8) // 将UInt8类型的200转换为Int64类型,结果为Int64类型的200
Int8(200) // 将Int64类型的200转换为Int8类型,溢出错误
然后,在浮点类型之间转换时也需要注意不同浮点类型的表示范围,如果是精度高的浮点类型向精度低的浮点类型转换,将会出现精度损失。举例如下:
main() {
let x: Float64 = 987.654321
let y = Float16(x)
println(x) // 输出:987.654321
println(y) // 输出:987.500000,损失了精度
}
最后,在整数类型和浮点类型相互转换时,要确保待转换的类型必须要在目标类型的表示范围之内。另外,如果是浮点类型转换为整数类型,那么浮点数的小数部分会被直接截断,只保留整数部分。举例如下:
Float64(2023) // 整数类型向浮点类型转换,结果为2023.0
Int16(7.9) // 浮点类型向整数类型转换,结果为7
对整数类型来说,Int64类型的表示范围最大,不容易引发溢出错误。另外,在没有类型上下文的情况下,Int64类型是仓颉默认的整数类型,而使用默认的类型可以避免在运算中进行不必要的类型转换。因此,在Int64类型适合的情况下,建议首选Int64类型。
对浮点类型来说,除了与整数类型相同的原因,由于浮点类型在存储和运算时都会产生一定的误差,因此精度高的浮点类型优于精度低的浮点类型。在多种浮点类型都适合的情况下,建议首选Float64类型。