仓颉是一门 静态类型语言(static typing),在编译时确定变量类型;
同时具备一定的 类型推断能力(type inference),减少显式声明负担。

仓颉的数据类型体系融合了:

  • Kotlin / Swift 的现代类型系统
  • Java 的 JVM 兼容性
  • Rust / Go 的内存安全与不可变性思维

因此,仓颉中的类型分为以下四大类:

分类

说明

基本类型(Primitive Types)

数值、字符、布尔、字符串等基础数据单元

复合类型(Composite Types)

数组、集合、映射、结构体、类

特殊类型(Special Types)

空类型、可选类型、Any、Nothing、Unit 等

自定义类型(User-defined Types)

用户定义的类(class)、枚举(enum)、接口(interface)、结构体(struct)

整数类型

Int仓颉的标准整型类型(32 位有符号整数),相当于:

  • Java 的 int
  • Kotlin 的 Int
  • C/C++ 的 int32_t

存储范围-2,147,483,648 ~ 2,147,483,647 (即 -2³¹ ~ 2³¹−1)

占用内存:4 字节(32 位)

Int 多种数值字面量形式

形式

示例

含义

十进制(Decimal)

let a = 42

默认形式

二进制(Binary)

let b = 0b101010

0b开头

八进制(Octal)

let c = 0o52

0o开头

十六进制(Hexadecimal)

let d = 0x2A

0x 开头

数值下划线分隔符

仓颉允许用 _ 分隔长数字,以增强可读性(不会影响值):

let big: Int = 1_000_000     // 一百万
let mask: Int = 0b1111_0000  // 二进制分组
let color: Int = 0xFF_FF_FF  // 十六进制颜色

Int 的特殊特性

特性

说明

默认整型

不带小数点的数字默认是 Int

自动类型推断

let a = 5

推断为 Int

不会自动溢出

溢出时编译器会检查警告或抛错(安全机制)

不可与 Double

自动混算

必须显式转换

有符号整数类型(Signed Integers)

类型名

位宽

数值范围

说明

Int8

8 位

-128 ~ 127

最小的有符号整数类型,常用于节省内存或处理字节级数据。

Int16

16 位

-32,768 ~ 32,767

常用于文件头、网络协议字段等短整数。

Int32

32 位

-2,147,483,648 ~ 2,147,483,647

默认的标准整数类型,大多数整型运算使用它。

Int64

64 位

-9,223,372,036,854,775,808 ~ 9,223,372,036,854,775,807

用于需要高精度或大范围数值的场景,如时间戳、文件大小、计数器等。

说明:

  • 所有有符号整数类型均使用二进制补码(Two’s Complement) 进行编码;
  • 在计算与类型转换中,小范围整数(如 Int8)可以安全地自动提升为更大范围的整数类型(如 Int32)。

无符号整数类型(Unsigned Integers)

类型名

位宽

数值范围

说明

UInt8

8 位

0 ~ 255

常用于表示原始字节数据或颜色通道。

UInt16

16 位

0 ~ 65,535

适合存储短整数的非负值。

UInt32

32 位

0 ~ 4,294,967,295

用于大型计数或标识符。

UInt64

64 位

0 ~ 18,446,744,073,709,551,615

用于极大范围的无符号整数。

说明:

  • 无符号整数不支持负数;
  • 在算术运算中,如果混合使用有符号与无符号类型,编译器会根据上下文推导或要求显式转换。

默认整数类型

在仓颉中,直接写整数常量如:

let a = 42

默认推断为 Int32 类型。若需特定位宽,可以通过后缀或显式类型注解指定:

let a: Int8 = 42
let b = 42i64  // 通过后缀指定 Int64

浮点数类型

在仓颉语言中,浮点类型用于表示带有小数部分或科学计数法的实数值。
这些类型基于 IEEE 754 标准实现,兼顾精度与性能。

一、浮点类型分类

类型名

位宽

精度

表示范围

说明

Float32

32 位

约 6~7 位十进制有效数字

±(1.18 × 10⁻³⁸) ~ ±(3.4 × 10³⁸)

单精度浮点数,节省内存,常用于图形、信号处理等场景。

Float64

64 位

约 15~16 位十进制有效数字

±(2.23 × 10⁻³⁰⁸) ~ ±(1.79 × 10³⁰⁸)

双精度浮点数,默认类型,适合高精度运算与科学计算。

在仓颉中,默认的浮点字面量(例如 3.142.0e10)类型为 Float64
若需指定 Float32,可以使用后缀或类型注解:

let x: Float32 = 2.14
let y = 2.14f32

二、浮点数的表示结构(IEEE 754 简述)

仓颉的浮点数遵循 IEEE 754 标准,由三部分组成:

部分

位数 (Float32)

位数 (Float64)

含义

符号位(Sign)

1

1

表示正负(0 为正,1 为负)

阶码(Exponent)

8

11

表示数值的指数部分(偏移编码)

尾数(Mantissa / Fraction)

23

52

表示小数的有效数字部分

例如:
-5.75 在二进制下会编码成 IEEE 754 结构存储,实际数值为:
−1)sign×1.mantissa×2(exponent−bias)-1)^{sign} × 1.mantissa × 2^{(exponent-bias)}−1)sign×1.mantissa×2(exponent−bias)


三、浮点常量与科学计数法

仓颉支持以下形式的浮点字面量:

let a = 2.14          // 普通小数
let b = 1.0e3         // 科学计数法 (1.0 × 10³)
let c = -1.5E-4       // 科学计数法 (−1.5 × 10⁻⁴)
let d: Float32 = 6.022e23

四、浮点计算规则

浮点计算遵循 IEEE 754 舍入与溢出规则:

操作

行为说明

加减乘除

标准运算符(+ - * /

)适用。结果可能因精度导致舍入误差。

取余 (%)

支持浮点取余,结果遵循 IEEE 754 定义。

比较 (==, >, <)

比较操作符会遵循 IEEE 规则,NaN 与任何值(包括自身)比较均为 false

隐式转换

整数可自动转换为 Float64,反之需显式转换。

示例:

let x = 0.1 + 0.2
print(x)  // 输出可能为 0.30000000000000004

let a = 5 / 2          // 结果为 1.5 (Float64)
let b: Int = 5
let c = Float64(b) / 1.0

五、特殊值与异常情况

浮点数计算中可能产生以下特殊值:

特殊值

含义

示例

NaN

(Not a Number)

非数值,表示非法计算结果,如 0.0 / 0.0

sqrt(-1)

Float64.NaN

Infinity

正无穷大

1.0 / 0.0

-Infinity

负无穷大

-1.0 / 0.0

-0.0

带符号的零,在计算中与 0.0

等价但在某些场景区分

1.0 / -Infinity


六、浮点比较的陷阱与解决方法

由于浮点数精度限制,直接比较可能导致逻辑错误:

let a = 0.1 + 0.2
if a == 0.3 {
    print("Equal")
} else {
    print("Not equal")  // 实际输出
}

解决方式是使用“误差容忍比较”:

let eps = 1e-9
if abs(a - 0.3) < eps {
    print("Approximately equal")
}

七、类型转换与混合计算

来源类型

目标类型

转换方式

Int → Float

自动

精度可能损失

Float → Int

显式(需调用 Int32(x)

向零取整

Float32 → Float64

显式转换

精度提升或丢失

示例:

let a: Int = 42
let b = Float64(a)       // 自动提升
let c = Int32(2.99)      // 截断为 3

八、NaN、无穷值判断函数

仓颉语言提供内置函数判断特殊浮点状态:

Float63.isNaN(x)
Float63.isInfinite(x)
Float63.isFinite(x)

示例:

let x = 1.0 / 0.0
if Float63.isInfinite(x) {
    print("Infinity detected")
}

九、总结与对比

特性

Float32

Float64

内存占用

4 字节

8 字节

精度

约 7 位

约 16 位

默认类型

性能

较快

稍慢

适用场景

图形处理、游戏、音频

科学计算、金融、机器学习

布尔类型

一、Bool 类型简介

在仓颉语言中,布尔类型的关键字为:

Bool

它只有两个取值:

  • true:表示逻辑“真”
  • false:表示逻辑“假”

布尔类型通常用于控制流程(ifwhile 等)、比较表达式以及逻辑运算中。


二、Bool 的定义与赋值

布尔变量的定义形式与其他基本类型一致:

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

你也可以让编译器自动推断类型:

let a = true      // 自动识别为 Bool
let b = false

三、Bool 的逻辑运算符

仓颉中的布尔类型支持标准逻辑运算符:

运算符

含义

示例

结果

!

逻辑非(NOT)

!true

false

&&

逻辑与(AND)

true && false

false

||

逻辑或(OR)

true || false

true

==

等于

true == false

false

!=

不等于

true != false

true

示例代码:

let a = true
let b = false

println(!a)          // 输出 false
println(a && b)      // 输出 false
println(a || b)      // 输出 true
println(a == b)      // 输出 false
println(a != b)      // 输出 true

四、在条件语句中的应用

布尔值是控制语句的核心。例如:

let isAdmin = true

if (isAdmin) {
    println("Welcome, administrator.")
} else {
    println("Access denied.")
}

或者在循环中使用:

var flag = true
var count = 0

while (flag) {
    println("Loop count: \(count)")
    count += 1
    if (count > 3) {
        flag = false
    }
}

五、与比较运算结合使用

任何比较表达式的结果都是一个 Bool

let a = 10
let b = 20

let result = a < b    // result 的类型为 Bool,值为 true
println(result)

常见比较运算符包括:

  • <><=>=
  • ==!=

这些都返回 Bool 类型结果。


六、Bool 与数值类型的关系

与 C/C++ 不同,仓颉中的 Bool 不能直接与数值互换
也就是说,不能直接将 Bool 用作整数,也不能用 1/0 替代 true/false

错误示例:

let x: Int = true    // ❌ 类型不兼容

正确示例:

let x: Int = if (true) 1 else 0

七、布尔表达式的短路求值

仓颉语言支持 短路逻辑(Short-Circuit Evaluation):

  • a && b 中,如果 afalse,则 b 不会被计算。
  • a || b 中,如果 atrue,则 b 不会被计算。

示例:

fun test(): Bool {
    println("function called")
    return true
}

let a = false && test()  // test() 不会被调用
let b = true || test()   // test() 不会被调用

八、布尔函数返回示例

Bool 常用作函数返回值类型:

fun isEven(n: Int): Bool {
    return n % 2 == 0
}

println(isEven(4))   // 输出 true
println(isEven(5))   // 输出 false

九、Bool 的可选类型与判空

Bool 被包裹在可选类型(?)中时,也可为空值:

let maybe: Bool? = null

if (maybe != null && maybe == true) {
    println("Condition is true")
}

字符类型

一、基本定义与特性

在仓颉语言中,Char 是一种原始数据类型(Primitive Type),用于表示单个 Unicode 字符
每个 Char 都是不可变的(immutable),内部对应一个 Unicode 码位(code point),可以是英文字母、汉字、符号或 Emoji。
字面量形式使用单引号 ' ' 表示,例如:

let a: Rune ='A'
let b: Rune ='中'
let c: Rune ='😊'

仓颉的 Char 拥有完整的 Unicode 支持,这意味着它能正确表示世界各语言文字与表情符号。

概念

关键字

特征

字符类型

Rune

表示单个 Unicode 字符

不存在的类型

Char

编译时报错

字符集合

String

由多个 Rune

组成

转换

String([Rune...])

从字符数组生成字符串

编码值

.codePoint()

获取 Unicode 值


二、转义字符与 Unicode 表示

仓颉的字符字面量支持常见的转义形式,用于表示特殊字符或不可见字符:
\n(换行)、\t(制表符)、\r(回车)、\'(单引号)、\"(双引号)、\\(反斜杠)。
此外,还可以通过 \uXXXX 直接表示 Unicode 码位:

let newline: Rune ='\n'
let chinese: Rune ='\u4E2D'  // '中'
println(chinese)

这种方式在国际化或符号处理时尤为常用,能够精确控制字符编码。


三、字符与整数(Unicode 码位)的互转

Char 实际上在底层对应一个整数值——Unicode 编码点。
你可以通过 .codePoint() 获取字符的数值表示,也能通过 Char.fromCodePoint() 从数值反向生成字符:

let c: Rune ='A'
println(c.codePoint())             // 输出 65
let d: Rune =Char.fromCodePoint(66)
println(d)                         // 输出 'B'

这种互转特性使 Char 能够轻松参与编码转换、加密或字符判断等逻辑。


四、比较与逻辑判断

Char 支持比较运算,因为 Unicode 编码是有序的。
这意味着可以直接使用 <>==!= 等操作符:

let a: Rune ='A'
let b: Rune ='B'
println(a < b)   // true
println(a == b)  // false

此外,Char 还提供一组便捷判断函数:

println('9'.isDigit())        // true
println('a'.isLetter())       // true
println('A'.isUpperCase())    // true
println(' '.isWhitespace())   // true

这些函数封装了常用的字符分类逻辑,适合用于语法分析或输入校验。


五、大小写转换与字符处理

仓颉内置字符大小写转换方法,适用于英文字符的规范化处理:

println('a'.toUpperCase())   // 输出 'A'
println('Z'.toLowerCase())   // 输出 'z'

此外,字符可直接与 String 类型协作,例如拼接或构造字符串:

let ch1: Rune ='仓'
let ch2: Rune ='颉'
let s: String = String([ch1, ch2])
println(s)                   // 输出 "仓颉"

这种与 String 的无缝转换,让字符操作自然融入文本处理场景。


六、字符的常见转义序列

仓颉的 Char 支持标准转义字符(与 C 系语言兼容):

转义序列

含义

Unicode

'\n'

换行符

U+000A

'\r'

回车符

U+000D

'\t'

制表符

U+0009

'\''

单引号

U+0027

'\"'

双引号

U+0022

'\\'

反斜杠

U+005C

'\uXXXX'

Unicode 字符

例如 '\u4E2D'

'中'

总结

Char 是仓颉语言中最小的文本单位,它的 Unicode 原生支持使得国际化、多语言及符号处理极其简洁。
无论是从字符串中提取单个字符、判断字符属性,还是在加密、解析器、协议处理等底层逻辑中,Char 都发挥着基础性作用。
在仓颉的类型体系中,CharString 一起构成了文本处理的核心,前者负责单点精准控制,后者负责整体表达与组合。

字符串类型

一:类型定义与基本特性
在仓颉语言中,String 是一个不可变(immutable)的引用类型,用于表示一串 UTF-8 编码的文本数据。每个 String 实例都由一系列 Unicode 字符组成,并在内存中以连续字节形式存储。与部分语言不同,仓颉的字符串严格区分文本和字节序列,因此 String 只承载文本语义,任何与二进制数据相关的处理需使用 ByteArray 等类型。


二:字符串字面量与转义规则
字符串字面量以双引号 " 括起,例如:let s = "仓颉语言";。仓颉支持常见的转义字符,包括 \n(换行)、\t(制表符)、\\(反斜杠)和 \"(引号)。同时支持 Unicode 转义形式 \u{4ED9} 表示单个字符。多行字符串可用三重引号 """...""" 包裹,中间的所有格式与换行都会被保留,适合定义模板或大段文本。


三:字符串的不可变性与内存模型
所有 String 在创建后不可修改,这意味着对字符串的拼接、截取、替换等操作实际上会生成新的字符串对象。此设计带来了线程安全与引用透明性,方便在并发环境下使用。同时,编译器对字面量字符串会进行常量折叠(interning),以减少重复内存分配。


四:常见操作与方法
String 类型提供多种内建方法,如 len() 获取长度(按字符数,而非字节数)、isEmpty() 判断是否为空、contains(substr) 判断是否包含子串、replace(old, new) 字符串替换、split(delimiter) 拆分为子串数组等。拼接可使用 + 运算符或模板字符串语法:

  1. 创建字符串
package demo

main(): Int64 {
    var str1:String =
    """
    hello world
    """
    var str2:String =
    """
    D:\\Read\\demo
    """
    var str3:String =#"D:\Read\demo"#
    println(st1)
    println(str2)
    println(str3)                  
    return 0
}
  1. 字符串拼接

五:与 Rune、Char、Array 的关系
仓颉语言中,String 由多个 Rune(Unicode码位)组成,而不是 Char。若需逐字符访问,可使用 for (r in str.runes) 遍历;若需按字节处理,可通过 str.bytes() 获取字节数组。与其他语言不同,仓颉并没有直接的 Char 类型来存放单个字符,单个字母应以 Rune 表示,因此字符串拼接或构造时需使用 String.fromRunes([r1, r2])


六:比较、编码与性能考虑
字符串支持 == 比较操作,基于 Unicode 值逐字符匹配;排序与哈希操作均遵循字典序。仓颉在底层实现上默认使用 UTF-8 编码,对多语言文本具有良好兼容性。对于高性能需求的场景,推荐预分配拼接缓冲区或使用 StringBuilder 类似的结构以避免频繁创建中间字符串对象,从而提升性能。

元组类型

一、元组的基本语法和定义方式

元组的声明与初始化

在仓颉编程语言中,** 元组(Tuple)** 是一种可以将多个不同类型的值组合成一个复合值的轻量级数据结构。元组类型使用(T1, T2, ..., TN)的形式表示,其中 T1 到 TN 可以是任意类型,不同类型间使用逗号(,)分隔。

基本语法格式

元组的定义使用圆括号将元素括起来,元素间用逗号分隔:

let tuple_name: (Type1, Type2, ..., TypeN) = (value1, value2, ..., valueN)

其中:

  • tuple_name 是元组变量名
  • Type1, Type2, ..., TypeN 是各元素的数据类型
  • value1, value2, ..., valueN 是对应类型的初始值
示例说明

以下是几个元组定义的示例:

// 基本元组定义(类型推断)
let tuple1 = (1, "hello", true)  // 类型为(Int64, String, Bool)
// 显式类型标注
let tuple2: (Int32, Float64) = (100, 2.14)
// 带标签的元组(命名元组)
let person = (name: "Alice", age: 30, isStudent: false)
// 空元组(Unit类型)
let empty = ()  // 类型为Unit
带标签的元组

仓颉支持为元组元素添加标签(名称),以提高代码的可读性。带标签的元组语法如下:

let tuple_name: (label1: Type1, label2: Type2, ..., labelN: TypeN) = (label1: value1, label2: value2, ..., labelN: valueN)

或简化为:

let tuple_name = (label1: value1, label2: value2, ..., labelN: valueN)

注意:对于一个元组类型,必须统一写类型参数名,或者统一不写类型参数名,不允许交替存在。例如:

let c: (name: String, Int64) = ("banana", 5)  // 错误,类型参数名使用不一致
元组的长度要求

仓颉中的元组至少是二元以上,即最少包含两个元素。例如:

let valid_tuple: (Int64, Float64) = (3, 2.141592)    // 有效,二元组
let valid_tuple3: (Int64, Float64, String) = (3, 2.141592, "PI")  // 有效,三元组
let invalid_tuple: Int64 = (3)  // 错误,不是元组
类型推断机制

仓颉编译器具有强大的类型推断能力,在定义元组时可以省略类型标注,编译器会根据初始值自动推断元组的类型。例如:

let inferred_tuple = (40.7128, -73.0060)  // 自动推断为(Double, Double)类型

二、元组的特性

不可变性(Immutable)

元组在仓颉中是不可变类型,具有以下特性:

  1. 内容不可修改:一旦定义了一个元组类型的实例,它的内容不能再被更新。例如:
var tuple = (true, false)
tuple[0] = false  // 错误,'tuple element' cannot be assigned
  1. 长度固定:元组的长度是固定的,即一旦创建,元组的大小不可改变。无法添加或删除元组中的元素。
  2. 引用语义:虽然元组本身不可变,但如果元组中的元素是可变对象(如数组、类实例等),那么这些对象的内容是可以被修改的。这称为浅不可变性
类型异构性

元组的一个重要特性是可以包含不同类型的元素,这种特性称为类型异构性。例如:

let heterogeneous_tuple = (1, "string", 2.14, true)  // 包含Int、String、Float、Bool类型

这种灵活性使得元组非常适合需要组合多种不同类型数据的场景。

有序性

元组中的元素按定义顺序排列,通过位置索引访问。索引从 0 开始,这意味着第一个元素的索引是 0,第二个是 1,依此类推。

内存布局

元组在内存中是连续存储的,所有元素存储在一个连续的内存块中。这种存储方式使得元组的访问速度非常快,但也决定了元组的大小在创建时就必须确定。

与其他数据结构的比较

数据结构

可变性

元素类型

长度特性

典型用途

元组

不可变

异构

固定

临时数据组合、多返回值

数组

可变

同质

可变

数据集合、算法处理

列表

可变

同质

可变

动态数据存储

三、元组的访问、修改和操作

索引访问

元组支持通过索引访问元素,索引从 0 开始,使用方括号[]操作符。例如:

let serverStatus = (200, "OK", true)
print(serverStatus[0])  // 输出:200
print(serverStatus[1])  // 输出:"OK"
print(serverStatus[2])  // 输出:true

注意:索引必须是整数字面量,否则会编译报错。例如:

let index = 0
print(serverStatus[index])  // 错误,索引不能是变量
命名访问(带标签元组)

对于带标签的元组,可以通过标签名访问元素:

let person = (name: "Alice", age: 30, isStudent: false)
print(person.name)    // 输出:Alice
print(person.age)     // 输出:30
解构赋值

元组最强大的特性之一是解构赋值,可以一次性将元组值分配给多个变量。

基本解构
let (statusCode, message, isSuccess) = serverStatus
print("Status: \(statusCode) \(message)")  // 输出:Status: 200 OK
部分解构

使用下划线_忽略不需要的值:

let (_, responseMessage, _) = serverStatus
print("Response: \(responseMessage)")  // 输出:Response: OK
嵌套解构

支持对嵌套元组进行解构:

func getConfig() -> (timeout: Int, (host: String, port: Int)) {
    return (30, ("api.example.com", 443))
}
let (timeout, (host, port)) = getConfig()
print("Connect to \(host):\(port) with timeout \(timeout)s")
修改元组的 "变通" 方法

由于元组的不可变性,不能直接修改元组的内容。但可以通过以下方式实现类似 "修改" 的效果:

创建新元组

通过创建一个新的元组来替换原来的元组:

var coordinates = (10.5, 20.3)
// 创建新元组,替换原来的坐标
coordinates = (coordinates.0, 24.0)  // 只修改第二个元素
print(coordinates)  // 输出:(10.5, 24.0)
使用可变容器

如果需要频繁修改数据,可以考虑使用其他可变数据结构,如数组或自定义类。

常见操作方法
元组比较

可以使用==和!=操作符比较两个元组是否相等,前提是元组长度相同,且对应位置的元素类型一致:

let tuple1: (Int, String) = (1, "Cangjie")
let tuple2: (Int, String) = (1, "Cangjie")
print("是否相等: \(tuple1 == tuple2)")  // 输出:是否相等: true
元组连接

可以使用+操作符连接两个元组:

let tupleA = (1, 2, 3)
let tupleB = (4, 5, 6)
let combined = tupleA + tupleB
print(combined)  // 输出:(1, 2, 3, 4, 5, 6)
元组解包

在函数参数传递时,可以直接解包元组:

func processData(a: Int, b: String, c: Bool) {
    print("a: \(a), b: \(b), c: \(c)")
}
let data = (42, "Hello", true)
processData(data.0, data.1, data.2)  // 传统方式
processData(...data)  // 解包方式
内置函数支持

仓颉为元组提供了一些内置函数:

len()函数

用于获取元组的长度(元素个数):

let tuple = (1, "two", 2.0)
print(len(tuple))  // 输出:3
转换函数
  • tuple():将其他可迭代对象转换为元组
元组的遍历

虽然元组本身不直接支持迭代,但可以通过以下方式实现遍历:

使用索引循环
let tuple = (1, "apple", 2.14)
for i in 0..<len(tuple) {
    print(tuple[i])
}
先转换为数组
let tuple = (1, 2, 3, 4)
let array = Array(tuple)
for element in array {
    print(element)
}

四、元组的应用场景

函数多返回值

元组最常见的应用场景之一是作为函数的多返回值。例如:

func getStats(arr: Array<Int64>): (min: Int64, max: Int64, avg: Float64) {
    // 计算数组的最小值、最大值和平均值
    var minVal = arr[0]
    var maxVal = arr[0]
    var sum = arr[0]
    
    for i in 1..<arr.size() {
        let val = arr[i]
        if val < minVal {
            minVal = val
        } else if val > maxVal {
            maxVal = val
        }
        sum += val
    }
    
    let avg = Float64(sum) / Float64(arr.size())
    return (minVal, maxVal, avg)
}
// 使用解构赋值获取多个返回值
let (min, max, avg) = getStats([1, 2, 3, 4, 5])
print("Min: \(min), Max: \(max), Average: \(avg)")
临时数据组合

元组适合用于临时组合相关数据,特别是在不需要定义复杂数据结构的情况下:

// 表示坐标点
let point = (x: 10.5, y: 20.3)
// 表示颜色(RGB)
let color = (red: 255, green: 128, blue: 64)
// 表示日期
let date = (year: 2024, month: 10, day: 15)
值交换

利用元组可以很方便地进行值交换:

var a = 5
var b = 10
(a, b) = (b, a)  // 优雅地交换两个变量的值
print("a: \(a), b: \(b)")  // 输出:a: 10, b: 5
模式匹配

元组在模式匹配中非常有用,可以方便地匹配不同的情况:

let point = (0, 5)
match point {
    case (0, 0) => print("原点")
    case (_, 0) => print("在X轴上")
    case (0, _) => print("在Y轴上")
    case (x, y) => print("在坐标(\(x), \(y))处")
}
作为字典键

由于元组的不可变性,它可以作为字典的键:

let coordinates = [
    (39.9042, 116.4074): "北京",
    (34.6895, 139.6917): "东京",
    (40.7128, -73.0060): "纽约"
]
print(coordinates[(39.9042, 116.4074)] ?? "未知地点")  // 输出:北京

总结

元组是仓颉编程语言中一种轻量级、不可变、类型安全的数据结构,它允许将多个不同类型的值组合成一个复合值。通过本文的详细介绍,我们了解到:

  1. 基本语法:元组使用(T1, T2, ..., TN)定义,至少包含两个元素,可以使用类型标注或依赖类型推断,支持带标签的命名元组。
  2. 核心特性:元组具有不可变性、类型异构性、有序性等特点,长度固定,内容不可修改,适合存储需要长期保持不变的数据。
  3. 操作方法:元组支持索引访问、解构赋值、比较、连接等操作。虽然不能直接修改元组内容,但可以通过创建新元组来实现类似效果。
  4. 应用场景:元组广泛应用于函数多返回值、临时数据组合、值交换、模式匹配、作为字典键等场景,能够显著提高代码的简洁性和可读性。
  5. 最佳实践:建议元组元素不超过 5 个,为重要元组使用有意义的标签,避免深层嵌套,在适当场景下使用以提高代码质量。

掌握元组的使用技巧,可以让你在仓颉编程中更加灵活高效地处理各种数据组合需求,写出更加优雅、简洁的代码。


数组类型

区间类型

一、基本语法

1.1 区间类型的声明与初始化

在仓颉编程语言中,** 区间类型(Range)** 是一种特殊的序列类型,用于表示拥有固定步长的连续数值序列。区间类型采用泛型设计,使用Range<T>表示,其中T是泛型参数,代表区间中元素的数据类型。

每个区间类型的实例包含三个核心属性:

  • start:序列的起始值,类型为T
  • end:序列的终止值,类型为T
  • step:序列中前后两个元素之间的差值(步长),类型为Int64,且不能为 0

区间类型的基本语法格式有两种:

左闭右开区间:start..end : step

表示从start开始,以step为步长,到end(不包含end)为止的区间。

左闭右闭区间:start..=end : step

表示从start开始,以step为步长,到end(包含end)为止的区间。

在实际使用中,步长step可以省略,此时默认值为 1。但需要注意的是,步长不能为 0,否则会引发编译错误。

1.2 区间类型的实例化方式

区间类型的实例化主要有两种方式:构造函数方式字面量方式

构造函数方式提供了最完整的控制能力,需要指定六个参数:

Range<T>(start: T, end: T, step: Int64, hasStart: Bool, hasEnd: Bool, isClosed: Bool)

其中:

  • hasStart和hasEnd分别表示是否包含起点和终点
  • isClosed表示是否为闭区间

以下是构造函数实例化的示例:

// 闭区间 [0, 10] 步长1
let r1 = Range<Int64>(0, 10, 1, true, true, true)  // 包含: 0,1,2,...,10
// 左闭右开 [0, 10) 步长1
let r2 = Range<Int64>(0, 10, 1, true, true, false)  // 包含: 0,1,2,...,9
// 左闭右开 [10, 0) 步长-2
let r3 = Range<Int64>(10, 0, -2, true, true, false)  // 包含: 10,8,6,4,2

字面量方式是仓颉推荐的用法,语法更加简洁明了:

let n = 10
// 左闭右开 [0, 10) 步长1
let r1 = 0..10 : 1      // 元素: 0,1,2,...,9
// 闭区间 [0, 10] 步长1
let r2 = 0..=n : 1      // 元素: 0,1,2,...,10
// 左闭右开 [10, 0) 步长-2
let r3 = n..0 : -2      // 元素: 10,8,6,4,2
// 闭区间 [10, 0] 步长-2
let r4 = 10..=0 : -2    // 元素: 10,8,6,4,2,0
1.3 支持的数据类型

区间类型的泛型参数T需要满足特定条件:

  • 支持关系运算符(如<, >, <=, >=)
  • 支持与Int64类型做加法运算

常用的实例化类型包括:

  • Range<Int64>:整数区间,是最常用的区间类型
  • Range<Float64>:浮点数区间,支持小数步长

需要特别注意的是,start和end必须是相同类型,而step固定为Int64类型。这意味着在创建浮点区间时,虽然start和end可以是浮点数,但步长仍然是整数。

1.4 区间类型的基本使用

区间类型在仓颉中常用于以下场景:

循环遍历

let range = 0..10 : 2  // 0, 2, 4, 6, 8
for (i in range) {
    println("元素: \(i)")
}

数组索引

let arr = [0, 1, 2, 3, 4, 5, 6]
let subArr = arr[0..5]  // 取前5个元素 [0,1,2,3,4]

条件判断

let age = 25
if (age in 18..=60) {
    println("成年人")
}

二、区间类型的核心特性

2.1 不可变性(Immutable)

区间类型在仓颉中是不可变类型,这意味着一旦创建,其三个核心属性start、end和step就不能被修改。这种设计带来了以下好处:

  1. 线程安全:不可变对象天生线程安全,可以在多线程环境中安全使用
  2. 性能优化:避免了可变对象的额外开销
  3. 数据一致性:确保区间内容不会意外改变

虽然区间对象本身不可变,但需要注意的是,变量的可变性由声明时使用的关键字决定:

  • 使用let声明的区间变量是不可变的
  • 使用var声明的区间变量是可变的,但只能重新赋值整个区间对象,不能修改其内部属性
let r1 = 0..10 : 1  // 不可变变量,不能重新赋值
var r2 = 10..0 : -1  // 可变变量,可以重新赋值
r2 = 5..15 : 2  // 合法,可以重新赋值
r2.step = 3     // 错误!不能修改区间的内部属性
2.2 包含性(边界规则)

仓颉的区间类型采用左闭右开左闭右闭两种边界规则,这是其最重要的特性之一:

左闭右开区间(start..end)

  • 包含起始值start
  • 不包含终止值end
  • 适用于数组索引等场景,与许多编程语言的习惯保持一致

左闭右闭区间(start..=end)

  • 包含起始值start
  • 包含终止值end
  • 适用于需要明确包含边界值的场景

这种设计既保持了与其他语言(如 Python、Swift)的兼容性,又提供了更大的灵活性。

2.3 步长特性

步长是区间类型的核心参数,具有以下特点:

  1. 类型固定:步长始终为Int64类型
  2. 非零要求:步长不能为 0,否则会引发编译错误
  3. 决定方向
    • 正步长表示递增序列
    • 负步长表示递减序列

步长的使用示例:

// 正步长 - 递增序列
let ascending = 0..10 : 3  // 0, 3, 6, 9
// 负步长 - 递减序列
let descending = 10..=0 : -2  // 10, 8, 6, 4, 2, 0
// 步长为1时可省略
let defaultStep = 5..15  // 等价于 5..15 : 1
2.4 空区间处理

当区间无法生成任何有效元素时,会返回空区间。仓颉对空区间有明确的判断规则:

左闭右开区间(start..end : step)为空的条件

  • 当step > 0且start >= end
  • 当step < 0且start <= end

左闭右闭区间(start..=end : step)为空的条件

  • 当step > 0且start > end
  • 当step < 0且start < end

空区间示例:

// 以下均为空区间
let empty1 = 10..0 : 1    // step>0但start>end
let empty2 = 0..10 : -1   // step<0但start<end
let empty3 = 10..=0 : 1   // step>0但start>end
let empty4 = 0..=10 : -1  // step<0但start<end

编译器会对空区间进行优化,在循环等场景中会直接跳过空区间的处理逻辑,避免无效计算。

2.5 类型兼容性

区间类型的泛型参数T需要满足特定的约束条件:

  1. 必须支持关系运算符(<, >, <=, >=)
  2. 必须支持与Int64类型的加法运算

这些约束确保了区间类型能够正确计算序列中的元素。例如,Int64和Float64类型都满足这些条件,因此可以创建整数区间和浮点区间:

// 整数区间
let intRange = 1..100 : 2  // Int64类型区间
// 浮点区间
let floatRange = 0.0..=3.0 : 0.5  // Float64类型区间
2.6 内存布局

区间类型在内存中是轻量级的数据结构,通常只占用几个机器字的存储空间(具体取决于泛型参数T的大小)。这种紧凑的内存布局带来了以下优势:

  1. 高效传递:区间对象在函数调用时可以高效传递
  2. 快速访问:可以快速访问start、end和step属性
  3. 低内存占用:相比其他序列类型(如数组),区间类型的内存占用极小
2.7 与其他数据结构的对比

为了更好地理解区间类型的特性,我们将其与其他常用数据结构进行对比:

数据结构

可变性

元素类型

内存占用

主要用途

区间(Range)

不可变

同质

极小

表示连续序列、循环控制

数组(Array)

可变

同质

存储大量数据、随机访问

元组(Tuple)

不可变

异构

组合不同类型的值

列表(List)

可变

同质

动态数据存储

三、区间类型的操作方法

3.1 元素访问与遍历

区间类型支持通过for-in循环进行遍历,这是最常用的访问方式:

let range = 2..8 : 2  // 2, 4, 6
for (num in range) {
    println("元素: \(num)")
}

需要注意的是,区间类型本身不支持随机访问(如range[2]),如果需要获取特定位置的元素,需要通过遍历的方式实现。

在某些情况下,可以将区间转换为数组来获得随机访问能力:

let arr = Array(0..100 : 3)  // 将区间转换为数组
print(arr[10])  // 输出第11个元素
3.2 区间的连接与合并

虽然区间类型本身不直接支持连接操作,但可以通过一些技巧实现类似功能:

合并两个递增区间

let range1 = 1..5 : 1
let range2 = 6..10 : 1
let combined = range1 + range2  // 错误!区间不支持直接相加
// 正确做法:转换为数组后连接
let arr1 = Array(range1)
let arr2 = Array(range2)
let combinedArr = arr1 + arr2

创建连续区间

如果需要创建一个大范围的连续区间,可以使用更长的区间表达式:

let bigRange = 1..1000000  // 直接创建大范围区间
3.3 区间的反转

虽然区间类型没有内置的反转方法,但可以通过调整start、end和step参数来实现反向区间:

// 正向区间
let forward = 1..5 : 1  // 1, 2, 3, 4
// 反向区间(方法1)
let backward1 = 4..0 : -1  // 4, 3, 2, 1
// 反向区间(方法2)
let backward2 = 5..1 : -1  // 5, 4, 3, 2(注意包含5)
3.4 区间的数学运算

区间类型支持一些数学相关的操作:

区间长度计算

可以通过公式计算区间的元素个数:

func rangeLength(range: Range<Int64>) -> Int64 {
    if range.step > 0 {
        return max(0, (range.end - range.start + range.step - 1) / range.step)
    } else {
        return max(0, (range.start - range.end + (-range.step) - 1) / (-range.step))
    }
}

区间交集判断

判断两个区间是否有交集:

func hasIntersection(r1: Range<Int64>, r2: Range<Int64>) -> Bool {
    // 简化的交集判断逻辑
    return !(r1.end <= r2.start || r2.end <= r1.start)
}
3.5 区间的转换

区间类型可以与其他数据结构进行转换:

转换为数组

let range = 0..10 : 2
let arr = Array(range)  // [0, 2, 4, 6, 8]

转换为字符串

let range = 1..=3
print(range.description)  // 输出: "1..=3"
3.6 特殊区间的创建

仓颉支持一些特殊形式的区间:

单侧区间(不完整区间):

虽然仓颉的标准区间需要指定start和end,但在某些上下文中可以使用简化形式:

// 在数组切片中使用不完整区间
let arr = [1, 2, 3, 4, 5]
let subArr1 = arr[..3]  // 前3个元素 [1,2,3]
let subArr2 = arr[2..]  // 从第3个元素开始到结束 [3,4,5]

浮点区间

let floatRange = 0.0..=3.0 : 0.5  // 0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0

需要注意的是,浮点区间可能存在精度问题,建议在步长能够整除的情况下使用。

四、最佳实践与注意事项

性能优化建议
  1. 优先使用字面量语法:字面量语法比构造函数更简洁、更易读,是日常编码的首选。
  2. 避免不必要的区间转换:虽然可以将区间转换为数组,但这会带来额外的内存分配和元素复制开销。在仅需要遍历的场景下,应直接使用区间。
  3. 利用编译器优化:编译器会对空区间进行优化,直接跳过相关处理逻辑。因此在编写循环时无需手动检查空区间。
  4. 选择合适的步长:步长的选择会影响区间的性能。过大的步长可能导致区间长度过小,而过小的步长则可能导致不必要的计算。
常见错误与陷阱
  1. 步长为 0 的错误:步长不能为 0,否则会引发编译错误。在动态生成步长时要特别注意这一点。
  2. 空区间陷阱:在使用区间前要注意检查是否为空区间,特别是当start和end是动态计算时。
  3. 类型不匹配:start和end必须是相同类型,在进行类型转换时要格外小心。
  4. 浮点精度问题:浮点区间可能存在精度问题,建议在步长能够整除的情况下使用,或使用整数表示(如毫秒代替秒)。
  5. 区间方向错误:当使用负步长时,要确保start大于end,否则会得到空区间。
与其他语言的对比

了解仓颉区间类型与其他语言的异同,有助于更好地理解和使用它:

与 Python 的对比

  • 仓颉和 Python 都支持左闭右开区间(range)
  • Python 的range只支持整数,而仓颉的区间支持多种类型
  • 仓颉增加了左闭右闭区间(..=),提供了更多灵活性

与 Swift 的对比

  • 两者都支持多种区间类型(闭区间、半开区间)
  • Swift 的Range是半开区间(a..<b),而仓颉的..也是半开区间
  • 仓颉的区间支持步长参数,而 Swift 需要通过其他方式实现

与 C++ 的对比

  • C++ 没有内置的区间类型,需要使用标准库的std::pair或std::span
  • 仓颉的区间类型更加高级,支持步长和多种边界规则

Unit 类型

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

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

Nothing 类型

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

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

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
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐