基本数据类型

本章介绍仓颉中的基本数据类型以及它们支持的基本操作,包括:整数类型、浮点类型、布尔类型、字符类型、字符串类型、Unit 类型、元组类型、区间类型、Nothing 类型。

整数类型

整数类型分为有符号(signed)整数类型和无符号(unsigned)整数类型。

有符号整数类型包括 Int8Int16Int32Int64IntNative,分别用于表示编码长度为 8-bit16-bit32-bit64-bit 和平台相关大小的有符号整数值的类型。

无符号整数类型包括 UInt8UInt16UInt32UInt64UIntNative,分别用于表示编码长度为 8-bit16-bit32-bit64-bit 和平台相关大小的无符号整数值的类型。

对于编码长度为 N 的有符号整数类型,其表示范围为:$$-2^{N-1} \sim 2^{N-1}-1$$;对于编码长度为 N 的无符号整数类型,其表示范围为:$$0 \sim 2^{N}-1$$。下表列出了所有整数类型的表示范围:

类型表示范围
Int8$$-2^7 \sim 2^7-1 (-128 \sim 127)$$
Int16$$-2^{15} \sim 2^{15}-1 (-32,768 \sim 32,767)$$
Int32$$-2^{31} \sim 2^{31}-1 (-2,147,483,648 \sim 2,147,483,647)$$
Int64$$-2^{63} \sim 2^{63}-1 (-9,223,372,036,854,775,808 \sim 9,223,372,036,854,775,807)$$
IntNativeplatform dependent
UInt8$$0 \sim 2^8-1 (0 \sim 255)$$
UInt16$$0 \sim 2^{16}-1 (0 \sim 65,535)$$
UInt32$$0 \sim 2^{32}-1 (0 \sim 4,294,967,295)$$
UInt64$$0 \sim 2^{64}-1 (0 \sim 18,446,744,073,709,551,615)$$
UIntNativeplatform dependent

程序具体使用哪种整数类型,取决于该程序中需要处理的整数的性质和范围。在 Int64 类型适合的情况下,首选 Int64 类型,因为 Int64 的表示范围足够大,并且整数字面量(在下一节介绍)在没有类型上下文的情况下默认推断为 Int64 类型,可以避免不必要的类型转换。

整数类型字面量

整数类型字面量有 4 种进制表示形式:二进制(使用 0b0B 前缀)、八进制(使用 0o0O 前缀)、十进制(没有前缀)、十六进制(使用 0x0X 前缀)。例如,对于十进制数 24,表示成二进制是 0b00011000(或 0B00011000),表示成八进制是 0o30(或 0O30),表示成十六进制是 0x18(或 0X18)。

在各进制表示中,可以使用下划线 _ 充当分隔符的作用,方便识别数值的位数,如 0b0001_1000

对于整数类型字面量,如果它的值超出了上下文要求的整数类型的表示范围,编译器将会报错。

let x: Int8 = 128          // Error: 128 out of the range of Int8
let y: UInt8 = 256         // Error: 256 out of the range of UInt8
let z: Int32 = 0x8000_0000 // Error: 0x8000_0000 out of the range of Int32

在使用整数类型字面量时,可以通过加入后缀来明确整数字面量的类型,后缀与类型的对应为:

后缀类型后缀类型
i8Int8u8UInt8
i16Int16u16UInt16
i32Int32u32UInt32
i64Int64u64UInt64

加入了后缀的整数字面量可以像下面的方式来使用:

var x = 100i8  // x is 100 with type Int8
var y = 0x10u64 // y is 16 with type UInt64
var z = 0o432i32  // z is 282 with type Int32

整数类型支持的操作

整数类型默认支持的操作符包括:算术操作符、位操作符、关系操作符、自增和自减操作符、赋值操作符、复合赋值操作符。各操作符的优先级和用法参见附录中的[操作符]。

  1. 算术操作符包括:一元负号(-)、加法(+)、减法(-)、乘法(*)、除法(/)、取模(%)、幂运算(**)。

    注意:

    • 除了一元负号(-)和幂运算(**),其他操作符要求左右操作数是相同的类型。

    • */+- 的操作数可以是整数类型或浮点类型。

    • % 的操作数只支持整数类型。

    • ** 的左操作数只能为 Int64 类型或 Float64 类型,并且:

      • 当左操作数类型为 Int64 时,右操作数只能为 UInt64 类型,表达式的类型为 Int64
      • 当左操作数类型为 Float64 时,右操作数只能为 Int64 类型或 Float64 类型,表达式的类型为 Float64

    幂运算的使用,见如下示例:

    let p1 = 2 ** 3               // p1 = 8
    let p2 = 2 ** UInt64(3 ** 2)  // p2 = 512
    let p3 = 2.0 ** 3.0           // p3 = 8.0
    let p4 = 2.0 ** 3 ** 2        // p4 = 512.0
    let p5 = 2.0 ** 3.0           // p5 = 8.0
    let p6 = 2.0 ** 3.0 ** 2.0    // p6 = 512.0
    
  2. 位操作符包括:按位求反(!)、左移(<<)、右移(>>)、按位与(&)、按位异或(^)、按位或(|)。注意,按位与、按位异或和按位或操作符要求左右操作数是相同的整数类型。

  3. 关系操作符包括:小于(<)、大于(>)、小于等于(<=)、大于等于(>=)、相等(==)、不等(!=)。要求关系操作符的左右操作数是相同的整数类型。

  4. 自增和自减操作符包括:自增(++)和自减(--)。注意,仓颉中的自增和自减操作符只能作为一元后缀操作符使用。

  5. 赋值操作符即 =,复合赋值操作符包括:+=-=*=/=%=**=<<=>>=&=^=|=

注:本章中我们所提及的某个类型支持的操作,均是指在没有[操作符重载]的前提下。

字符字节字面量

仓颉编程语言引入了字符字节字面量来更为方便地用 ASCII 码来表示 UInt8 类型的值,由字符 b 和被单引号引用的值组成。例如:

var a = b'x' // a is 120 with type UInt8
var b = b'\n' // b is 10 with type UInt8
var c = b'\u{78}' // c is 120 with type UInt8

对于 \u 转义的字符不支持 Unicode,所以内部最多有两位 16 进制数。

浮点类型

浮点类型包括 Float16Float32Float64,分别用于表示编码长度为 16-bit32-bit64-bit 的浮点数(带小数部分的数字,如 3.14159、8.24 和 0.1 等)的类型。Float16Float32Float64 分别对应 IEEE 754 中的半精度格式(即 binary16)、单精度格式(即 binary32)和双精度格式(即 binary64)。

Float64 的精度约为小数点后 15 位,Float32 的精度约为小数点后 6 位,Float16 的精度约为小数点后 3 位。使用哪种浮点类型,取决于代码中需要处理的浮点数的性质和范围。在多种浮点类型都适合的情况下,首选精度高的浮点类型,因为精度低的浮点类型的累计计算误差很容易扩散,并且它能精确表示的整数范围也很有限。

浮点类型字面量

浮点类型字面量有两种进制表示形式:十进制、十六进制。在十进制表示中,一个浮点字面量至少要包含一个整数部分或一个小数部分,没有小数部分时必须包含指数部分(以 eE 为前缀,底数为 10)。在十六进制表示中,一个浮点字面量除了至少要包含一个整数部分或小数部分(以 0x0X 为前缀),同时必须包含指数部分(以 pP 为前缀,底数为 2)。

下面的例子展示了浮点字面量的使用:

let a: Float32 = 3.14
let b: Float32 = 2e3
let c: Float32 = 2.4e-1
let d: Float64 = .123e2
let e: Float64 = 0x1.1p0
let f: Float64 = 0x1p2
let g: Float64 = 0x.2p4

在使用十进制浮点数字面量时,可以通过加入后缀来明确浮点数字面量的类型,后缀与类型的对应为:

后缀类型
f16Float16
f32Float32
f64Float64

加入了后缀的浮点数字面量可以像下面的方式来使用:

let a = 3.14f32   // a is 3.14 with type Float32
let b = 2e3f32    // b is 2e3 with type Float32
let c = 2.4e-1f64 // c is 2.4e-1 with type Float64
let d = .123e2f64 // d is .123e2 with type Float64

浮点类型支持的操作

浮点类型默认支持的操作符包括:算术操作符、关系操作符、赋值操作符、复合赋值操作符。浮点类型不支持自增和自减操作符。

布尔类型

布尔类型使用 Bool 表示,用来表示逻辑中的真和假。

布尔类型字面量

布尔类型只有两个字面量:truefalse

下面的例子展示了布尔字面量的使用:

let a: Bool = true
let b: Bool = false

布尔类型支持的操作

布尔类型支持的操作符包括:逻辑操作符(逻辑非 !,逻辑与 &&,逻辑或 ||)、部分关系操作符(==!=)、赋值操作符、部分复合赋值操作符(&&=||=)。

字符类型

字符类型使用 Char 表示,可以表示 Unicode 字符集中的所有字符。

当前,仓颉已经引入了 RuneRuneChar 的 类型别名,定义为 type Rune = Char。

Rune 的语义与 Char 相同,都是 Unicode Scalar Value。目前 RuneChar 短期共存,但是将来 Char 将会被删除,建议需要使用字符类型的地方使用 Rune

字符类型字面量

字符类型字面量有三种形式:单个字符、转义字符和通用字符,它们均使用一对单引号定义。

单个字符的字符字面量举例:

let a: Char = 'a'
let b: Char = 'b'

转义字符是指在一个字符序列中对后面的字符进行另一种解释的字符。转义字符使用转义符号 \ 开头,后面加需要转义的字符。举例如下:

let slash: Char = '\\'
let newLine: Char = '\n'
let tab: Char = '\t'

通用字符以 \u 开头,后面加上定义在一对花括号中的 1~8 个十六进制数,即可表示对应的 Unicode 值代表的字符。举例如下:

main() {
    let he: Char = '\u{4f60}'
    let llo: Char = '\u{597d}'
    print(he)
    print(llo)
}

编译并执行上述代码,输出结果为:

你好

字符类型支持的操作

字符类型仅支持关系操作符:小于(<)、大于(>)、小于等于(<=)、大于等于(>=)、相等(==)、不等(!=)。比较的是字符的 Unicode 值。

字符串类型

字符串类型使用 String 表示,用于表达文本数据,由一串 Unicode 字符组合而成。

字符串字面量

字符串字面量分为三类:单行字符串字面量,多行字符串字面量,多行原始字符串字面量。

单行字符串字面量的内容定义在一对双引号之内,双引号中的内容可以是任意数量的(除了非转义的双引号和单独出现的 \ 之外的)任意字符。单行字符串字面量只能写在同一行,不能跨越多行。举例如下:

let s1: String = ""
let s2 = "Hello Cangjie Lang"
let s3 = "\"Hello Cangjie Lang\""
let s4 = "Hello Cangjie Lang\n"

多行字符串字面量以三个双引号开头,并以三个双引号结尾,并且开头的三个双引号之后需要换行(否则编译报错)。字面量的内容从开头的三个双引号换行后的第一行开始,到结尾的三个双引号之前为止,之间的内容可以是任意数量的(除单独出现的 \ 之外的)任意字符。不同于单行字符串字面量,多行字符串字面量可以跨越多行。举例如下:

let s1: String = """
    """
let s2 = """
    Hello,
    Cangjie Lang"""

多行原始字符串字面量以一个或多个井号(#)加上一个双引号开始,并以一个双引号加上和开始相同个数的 # 结束。开始的双引号和结束的双引号之间的内容可以是任意数量的任意合法字符。不同于(普通)多行字符串字面量,多行原始字符串字面量中的内容会维持原样(转义字符不会被转义,如下例中 s2 中的 \n 不是换行符,而是由 \n 组成的字符串 \n)。举例如下:

let s1: String = #""#
let s2 = ##"\n"##
let s3 = ###"
    Hello,
    Cangjie
    Lang"###

插值字符串

插值字符串是一种包含一个或多个插值表达式的字符串字面量(不适用于多行原始字符串字面量),通过将表达式插入到字符串中,可以有效避免字符串拼接的问题。虽然我们直到现在才介绍它,但其实它早已经出现在之前的示例代码中,因为我们经常在 println 函数中输出非字符串类型的变量值,例如 println("${x}")

插值表达式必须用花括号 {} 包起来,并在 {} 之前加上 $ 前缀。{} 中可以包含一个或者多个声明或表达式。

当插值字符串求值时,每个插值表达式所在位置会被 {} 中的最后一项的值替换,整个插值字符串最终仍是一个字符串。

下面是插值字符串的简单示例:

main() {
    let fruit = "apples"
    let count = 10
    let s = "There are ${count * count} ${fruit}"
    println(s)

    let r = 2.4
    let area = "The area of a circle with radius ${r} is ${let PI = 3.141592; PI * r * r}"
    println(area)
}

编译并执行上述代码,输出结果为:

There are 100 apples
The area of a circle with radius 2.400000 is 18.095570

字符串类型支持的操作

字符串类型支持使用关系操作符进行比较,支持使用 + 进行拼接。下面的例子展示了字符串类型的判等和拼接:

main() {
    let s1 = "abc"
    var s2 = "ABC"
    let r1 = s1 == s2
    println("The result of 'abc' == 'ABC' is: ${r1}")
    let r2 = s1 + s2
    println("The result of 'abc' + 'ABC' is: ${r2}")
}

编译并执行上述代码,输出结果为:

The result of 'abc' == 'ABC' is: false
The result of 'abc' + 'ABC' is: abcABC

字符串还支持其他常见操作,例如拆分、替换等,可参见标准库文档。

字节数组字面量

仓颉编程语言引入了字节数组字面量来方便地表达 Array<UInt8> 类型的值,由字符 b 、一对用双引号引用的 ASCII 字符串组成。例如:

var a = b"hello"

表示一个长度为 5,类型为 Array<UInt8> 的数组,其中的每个元素的值为对应字符的 ASCII 的数值,即 [104, 101, 108, 108, 111]

同时也支持转义字符,例如:

var b = b"\u{78}\n\r"

表示一个长度为 3,类型为 Array<UInt8> 的数组,其中的元素的值为 [120, 10, 13]

这里 \u 转义的字符和字符字节字面量相同,也不支持 Unicode,所以内部最多有两位 16 进制数。

值类型数组 VArray

仓颉编程语言引入了值类型数组 VArray<T, $N> ,其中 T 表示该值类型数组的元素类型,$N 是一个固定的语法,通过 $ 加上一个数值字面量表示这个值类型数组的长度。需要注意的是,VArray<T, $N> 不能省略 <T, $N>,且使用类型别名时,不允许拆分 VArray 关键字与其泛型参数。

type varr1 = VArray<Int64, $3> // ok
type varr2 = VArray // error

注意: 由于运行时后端限制,当前 VArray<T, $N> 的元素类型 TT 的成员不能包含引用类型、枚举类型、Lambda 表达式(CFunc 除外)以及未实例化的泛型类型。

VArray 可以由一个数组的字面量来进行初始化,左值 a 必须标识出 VArray 的实例化类型:

var a: VArray<Int64, $3> = [1, 2, 3]

同时,它拥有两个构造函数:

// VArray<T, $N>(initElement: (Int64) -> T)
let b = VArray<Int64, $5>({ i => i}) // [0, 1, 2, 3, 4]
// VArray<T, $N>(item!: T)
let c = VArray<Int64, $5>(item: 0) // [0, 0, 0, 0, 0]

除此之外,VArray<T, $N> 类型提供了两个成员方法:

1、用于下标访问和修改的 [] 操作符方法:

var a: VArray<Int64, $3> = [1, 2, 3]
let i = a[1] // i is 2
a[2] = 4 // a is [1, 2, 4]

2、用于获取 VArray 长度的 size 成员:

var a: VArray<Int64, $3> = [1, 2, 3]
let s = a.size // s is 3

Unit 类型

对于那些只关心副作用而不关心值的表达式,它们的类型是 Unit。例如,print 函数、赋值表达式、复合赋值表达式、自增和自减表达式、循环表达式,它们的类型都是 Unit

Unit 类型只有一个值,也是它的字面量:()。除了赋值、判等和判不等外,Unit 类型不支持其他操作。

元组类型

元组(Tuple)可以将多个不同的类型组合在一起,成为一个新的类型。元组类型使用 (T1, T2, ..., TN) 表示,其中 T1TN 可以是任意类型,不同类型间使用逗号(,)连接。元组至少是二元以上,例如,(Int64, Float64) 表示一个二元组类型,(Int64, Float64, String) 表示一个三元组类型。

元组的长度是固定的,即一旦定义了一个元组类型的实例,它的长度不能再被更改。

元组类型是不可变类型,即一旦定义了一个元组类型的实例,它的内容不能再被更新。例如

var tuple = (true, false)
tuple[0] = false // Error: 'tuple element' can not be assigned

元组类型的字面量

元组类型的字面量使用 (e1, e2, ..., eN) 表示,其中 e1eN 是表达式,多个表达式之间使用逗号分隔。下面的例子中,分别定义了一个 (Int64, Float64) 类型的变量 x,以及一个 (Int64, Float64, String) 类型的变量 y,并且使用元组类型的字面量为它们定义了初值:

let x: (Int64, Float64) = (3, 3.141592)
let y: (Int64, Float64, String) = (3, 3.141592, "PI")

元组支持通过 t[index] 的方式访问某个具体位置的元素,其中 t 是一个元组,index 是下标,并且 index 只能是从 0 开始且小于元组元素个数的整数类型字面量,否则,编译报错。下面的例子中,使用 pi[0]pi[1] 可以分别访问二元组 pi 的第一个元素和第二个元素。

main() {
    var pi = (3.14, "PI")
    println(pi[0])
    println(pi[1])
}

编译并执行上述代码,输出结果为:

3.140000
PI

在赋值表达式中,可使用元组字面量对表达式的右值进行解构,这要求赋值表达式等号左边必须是一个元组字面量,这个元组字面量里面的元素必须都是左值(左值即出现在赋值操作符左边的,可保存值的表达式,具体参见各章节对赋值操作的描述)或者一个元组字面量,当元组字面量中出现 _ 时,表示忽略等号右侧 tuple 对应位置处的求值结果(意味着这个位置处的类型检查总是可以通过的),等号右边的表达式也必须是 tuple 类型,右边 tuple 每个元素的类型必须是对应位置左值类型的子类型。注意,复合赋值不支持这种解构方式。求值顺序上先计算等号右边表达式的值,再对左值部分从左往右逐个赋值,例如

var a: Int64
var b: String
var c: Unit
var f = { => ((1, "abc"), ())}
((a, b), c) = f() // value of a is 1, value of b is "abc", value of c is '()'
((a, b), _) = ((2, "def"), 3.0) // value of a is 2, value of b is "def", 3.0 is ignored

元组类型的类型参数

可以为元组类型标记显式的类型参数名,下面例子中的 nameprice 就是 类型参数名

func getFruitPrice (): (name: String, price: Int64) {
    return ("banana", 10)
}

对于一个元组类型,只允许统一写类型参数名,或者统一不写类型参数名,不允许交替存在。

let c: (name: String, Int64) = ("banana", 5)   // error

区间类型

区间类型用于表示拥有固定步长的序列,区间类型是一个泛型(详见泛型章节),使用 Range<T> 表示。当 T 被实例化不同的类型时(要求此类型必须支持关系操作符,并且可以和 Int64 类型的值做加法),会得到不同的区间类型,如最常用的 Range<Int64> 用于表示整数区间。

每个区间类型的实例都会包含 startendstep 三个值。其中,startend 分别表示序列的起始值和终止值,step 表示序列中前后两个元素之间的差值(即步长);startend 的类型相同(即 T 被实例化的类型),step 类型是 Int64

区间类型字面量

区间字面量有两种形式:“左闭右开”区间和“左闭右闭”区间。其中,“左闭右开”区间的格式是 start..end : step,它表示一个从 start 开始,以 step 为步长,到 end(不包含 end)为止的区间;“左闭右闭”区间的格式是 start..=end : step,它表示一个从 start 开始,以 step 为步长,到 end(包含 end)为止的区间。

下面的例子定义了若干区间类型的变量:

let n = 10
let r1 = 0..10 : 1   // r1 contains 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
let r2 = 0..=n : 1   // r2 contains 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10
let r3 = n..0 : -2   // r3 contains 10, 8, 6, 4, 2
let r4 = 10..=0 : -2 // r4 contains 10, 8, 6, 4, 2, 0

区间字面量中,可以不写 step,此时 step 默认等于 1,但是注意,step 的值不能等于 0。另外,区间也有可能是空的(即不包含任何元素的空序列),举例如下:

let r5 = 0..10   // the step of r5 is 1, and it contains 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
let r6 = 0..10 : 0 // Error: step cannot be 0

let r7 = 10..0 : 1 // r7 to r10 are empty ranges
let r8 = 0..10 : -1
let r9 = 10..=0 : 1
let r10 = 0..=10 : -1

注:表达式 start..end : step 中,当 step > 0start >= end,或者 step < 0start <= end 时,start..end : step 是一个空区间;表达式 start..=end : step 中,当 step > 0start > end,或者 step < 0start < end 时,start..=end : step 是一个空区间。

Nothing 类型

Nothing 是一种特殊的类型,它不包含任何值,并且 Nothing 类型是所有类型的子类型。

breakcontinuereturnthrow 表达式的类型是 Nothing,程序执行到这些表达式时,它们之后的代码将不会被执行。其中 breakcontinue 只能在循环体中使用,return 只能在函数体中使用。

包围着的循环体”无法穿越“函数边界。在下面的例子中,break 出现在函数 f 中,外层的 while 循环体不被视作包围着它的循环体;continue 出现在 lambda 表达式 中,外层的 while 循环体不被视作包围着它的循环体。

while (true) {
    func f() {
        break // Error: break must be used directly inside a loop
    }
    let g = { =>
        continue // Error: continue must be used directly inside a loop
    }
}

由于函数的形参和其默认值不属于该函数的函数体,所以下面例子中的 return 表达式缺少包围它的函数体——它既不属于外层函数 f(因为内层函数定义 g 已经开始),也不在内层函数 g 的函数体中:

func f() {
    func g(x!: Int64 = return) { // Error: return must be used inside a function body
        0
    }
    1
}

注:目前编译器还不允许在使用类型的地方显式地使用 Nothing 类型。