仓颉-基础数据类型

整数类型

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

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

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

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

整数类型字面量

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

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

后缀 类型 后缀 类型
i8 Int8 u8 UInt8
i16 Int16 u16 UInt16
i32 Int32 u32 UInt32
i64 Int64 u64 UInt64

字符字节字面量

仓颉编程语言支持字符字节字面量,以方便使用 ASCII 码表示 UInt8 类型的值。字符字节字面量由字符 b、一对标识首尾的单引号、以及一个 ASCII 字符组成,例如:

1
2
3
4
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
c = b'\u{90}' - b'\u{66}' + c // c is 162 with type UInt8

b’x’ 表示类型为 UInt8 大小是 120 的字面值。另外还可以通过 b’\u{78}’ 这种转义形式表示类型为 UInt8,16 进制大小为 0x78 或 10 进制大小为 120 的字面值。需要注意的是,\u 内部最多有两位 16 进制数,并且值必须小于 256(十进制)。

整数类型支持的操作

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

浮点类型

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

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

浮点类型字面量

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

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

后缀 类型
f16 Float16
f32 Float32
f64 Float64

浮点类型支持的操作

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

浮点类型之间、浮点类型和整数类型之间可以互相转换,具体的类型转换语法及规则请参见数值类型之间的转换。

布尔类型

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

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

布尔类型支持的操作

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

字符类型

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

字符类型字面量

字符类型字面量有三种形式:单个字符、转义字符和通用字符。一个 Rune 字面量由字符 r 开头,后跟一个由一对单引号或双引号包含的字符。

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

1
2
let a: Rune = r'a'
let b: Rune = r"b"

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

1
2
3
let slash: Rune = r'\\'
let newLine: Rune = r'\n'
let tab: Rune = r'\t'

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

1
2
3
4
5
6
main() {
let he: Rune = r'\u{4f60}'
let llo: Rune = r'\u{597d}'
print(he)
print(llo)
}

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

字符类型支持的操作

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

Rune 可以转换为 UInt32,整数类型可以转换为 Rune

字符串类型

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

字符串字面量

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

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

1
2
3
4
let s1: String = ""
let s2 = 'Hello Cangjie Lang'
let s3 = "\"Hello Cangjie Lang\""
let s4 = 'Hello Cangjie Lang\n'

多行字符串字面量开头结尾需各存在三个双引号(""")或三个单引号(''')。字面量的内容从开头的三个引号换行后的第一行开始,到遇到的第一个非转义的三个引号为止,之间的内容可以是任意数量的(除单独出现的 \ 之外的)任意字符。不同于单行字符串字面量,多行字符串字面量可以跨越多行。举例如下:

1
2
3
4
5
let s1: String = """
"""
let s2 = '''
Hello,
Cangjie Lang'''

多行原始字符串字面量以一个或多个井号(#)和一个单引号(')或双引号(")开头,后跟任意数量的合法字符,直到出现与字符串开头相同的引号和与字符串开头相同数量的井号为止。在当前文件结束之前,如果还没遇到匹配的双引号和相同个数的井号,则编译报错。与多行字符串字面量一样,原始多行字符串字面量可以跨越多行。不同之处在于,转义规则不适用于多行原始字符串字面量,字面量中的内容会维持原样(转义字符不会被转义,如下例中 s2 中的 \n 不是换行符,而是由 \n 组成的字符串 \n)。举例如下:

1
2
3
4
5
6
let s1: String = #""#
let s2 = ##'#'\n'## // 输出结果为:#'\n
let s3 = ###"
Hello,
Cangjie
Lang"### // 该变量当中的换行、缩进等也会被保留

对于形如 left = right 的赋值操作,如果左操作数的类型是 Byte(内置类型 UInt8 的别名),并且右操作数是一个表示 ASCII 字符的字符串字面量,那么右操作数的字符串将分别被强制转换为 Byte 类型,再进行赋值;如果左操作数的类型是 Rune,并且右操作数是一个单字符的字符串字面量,那么右操作数的字符串将分别被强制转换为 Rune 类型,再进行赋值。

1
2
3
4
5
6
7
8
9
10
main() {
var b: Byte = "0"
print(b)
b = "1"
print(b)
var r: Rune = "0"
print(r)
r = "1"
print(r)
}

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

1
484901

插值字符串

插值字符串是一种包含一个或多个插值表达式的字符串字面量(不适用于多行原始字符串字面量),通过将表达式插入到字符串中,可以有效避免字符串拼接的问题。插值字符串经常出现在 println 函数中输出非字符串类型的变量值,例如 println("${x}")

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

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

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

1
2
3
4
5
6
7
8
9
10
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)
}

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

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

字符串类型支持的操作

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

1
2
3
4
5
6
7
8
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}")
}

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

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

字符串还支持其他常见操作,例如拆分、替换等。具体操作可以参考《仓颉编程语言标准库 API 》的 String 介绍,下面给出部分常见操作:

1
2
3
4
5
6
7
8
9
10
11
main() {
var s1 = "abc"
var s2 = "ABCabc"
var s3 = "abcyyabcqqabcbc"
let r1 = s2.contains(s1) // 判断s2中是否包含字符串s1
println(r1) // true
let r2 = s3.split(s1) //对原字符串 s3 按照字符串 s1 分隔符分割,指定是否删除空串
println(r2[1]) // yy
s1 = s2
println(s1) // ABCabc
}

元组类型

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

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

元组类型是不可变类型,即一旦定义了一个元组类型的实例,它的内容(即单个元素)不能再被更新。但整个元组可被覆盖替换,例如:

1
2
3
4
5
6
7
8
9
let tuple1 = (8, false)
var tuple2 = (true, 9, 20)
tuple2 = tuple1 // Error, mismatched types
tuple2[0] = false // Error, 'tuple element' can not be assigned

var tuple3 = (9, true)
tuple3 = tuple1
println(tuple3[0]) // 8
println(tuple3[1]) // false

元组类型的字面量

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

1
2
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 的第一个元素和第二个元素。

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

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

1
2
3.140000
PI

在赋值表达式中,可使用元组进行多赋值,参见赋值操作符章节。

元组类型的类型参数

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

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

对于一个元组类型,只允许统一写类型参数名,或者统一不写类型参数名,不允许交替存在,并且参数名本身不能作为变量使用或用于访问元组中元素。

1
2
3
let a: (name: String, Int64) = ("banana", 5)   // Error
let b: (name: String, price: Int64) = ("banana", 5) // OK
b.name // Error

数组类型

Array

可以使用 Array 类型来构造单一元素类型,有序序列的数据。

1
2
var a: Array<Int64> = [0, 0, 0, 0] // Array whose element type is Int64
var b: Array<String> = ["a1", "a2", "a3"] // Array whose element type is String

可以轻松使用字面量来初始化一个 Array,只需要使用方括号将逗号分隔的值列表括起来即可。

编译器会根据上下文自动推断 Array 字面量的类型。

1
2
let a: Array<String> = [] // Created an empty Array whose element type is String
let b = [1, 2, 3, 3, 2, 1] // Created a Array whose element type is Int64, containing elements 1, 2, 3, 3, 2, 1

也可以使用构造函数的方式构造一个指定元素类型的 Array。其中,repeat 属于 Array 构造函数中的一个命名参数。

需要注意的是,当通过 repeat 指定的初始值初始化 Array 时,该构造函数不会拷贝 repeat,如果 repeat 是一个引用类型,构造后数组的每一个元素都将指向相同的引用。

1
2
3
let a = Array<Int64>() // Created an empty Array whose element type is Int64
let c = Array<Int64>(3, repeat: 0) // Created an Array whose element type is Int64, length is 3 and all elements are initialized as 0
let d = Array<Int64>(3, {i => i + 1}) // Created an Array whose element type is Int64, length is 3 and all elements are initialized by the initialization function

示例中 let d = Array<Int64>(3, {i => i + 1}) 使用了 lambda 表达式作为初始化函数来初始化数组中的每一个元素,即 {i => i + 1}

访问 Array 成员

当需要对 Array 的所有元素进行访问时,可以使用 for-in 循环遍历 Array 的所有元素。Array 是按元素插入顺序排列的,因此对 Array 遍历的顺序总是恒定的。

当需要知道某个 Array 包含的元素个数时,可以使用 size 属性获得对应信息。

修改 Array

Array 是一种长度不变的 Collection 类型,因此 Array 没有提供添加和删除元素的成员函数。

但是 Array 允许对其中的元素进行修改,同样使用下标语法。

1
2
3
4
5
main() {
let arr = [0, 1, 2, 3, 4, 5]
arr[0] = 3
println("The first element is ${arr[0]}")
}

Array 虽然是 struct 类型,但其内部持有的只是元素的引用,因此在作为表达式使用时不会拷贝副本,同一个 Array 实例的所有引用都会共享同样的元素数据。

因此对 Array 元素的修改会影响到该实例的所有引用。

VArray

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

与频繁使用引用类型 Array 相比,使用值类型 VArray 可以减少堆上内存分配和垃圾回收的压力。但是需要注意的是,由于值类型本身在传递和赋值时的拷贝,会产生额外的性能开销,因此建议不要在性能敏感场景使用较大长度的 VArray。值类型和引用类型的特点请参见值类型和引用类型变量

1
2
type varr1 = VArray<Int64, $3> // Ok
type varr2 = VArray // Error

注意:

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

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

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

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

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

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

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

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

    下标访问的下标类型必须为 Int64

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

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

    size 属性的类型为 Int64

此外,VArray 还支持仓颉与 C 语言互操作场景使用,相关内容请参见数组

区间类型

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

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

下面的例子给出了区间类型的实例化方式(关于区间类型定义和其中的属性,详见《仓颉编程语言库 API》):

1
2
3
4
// Range<T>(start: T, end: T, step: Int64, hasStart: Bool, hasEnd: Bool, isClosed: Bool)
let r1 = Range<Int64>(0, 10, 1, true, true, true) // r1 contains 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10
let r2 = Range<Int64>(0, 10, 1, true, true, false) // r2 contains 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
let r3 = Range<Int64>(10, 0, -2, true, true, false) // r3 contains 10, 8, 6, 4, 2

区间类型字面量

区间字面量有两种形式:“左闭右开”区间和“左闭右闭”区间。

  • “左闭右开”区间的格式是 start..end : step,它表示一个从 start 开始,以 step 为步长,到 end(不包含 end)为止的区间;
  • “左闭右闭”区间的格式是 start..=end : step,它表示一个从 start 开始,以 step 为步长,到 end(包含 end)为止的区间。

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

1
2
3
4
5
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。另外,区间也有可能是空的(即不包含任何元素的空序列),举例如下:

1
2
3
4
5
6
7
let r5 = 0..10   // the step of r5 is 1, and r5 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 是一个空区间。

Unit 类型

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

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

Nothing 类型

Nothing 是一种特殊的类型,它不包含任何值,并且 Nothing 类型是所有类型的子类型(这当中也包括 Unit 类型)。

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

1
2
3
4
5
6
7
8
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 的函数体中(该用例相关内容,请参考嵌套函数):

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

注意:

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