仓颉编程语言青少年基础教程:数组类型
数组本质上是有序、同类型数据的集合容器,其核心作用是高效组织、访问和处理批量数据,同时结合语言特性,为开发者提供简洁、高性能的数据管理方式。例如:
main() {
let v1: Array<String> = ["a1", "a2", "a3"] // 使用 Array<String>
println(v1[0]) // 成功输出 a1
}
在仓颉语言中,“数组” 相关的类型主要包括 Array、VArray 和 ArrayList 三种,它们虽名称或特性不同,但都用于组织有序的元素集合,只是在可变性、存储方式和适用场景上有显著差异。
三种类型的核心区别与选择
类型 | 长度特性 | 类型性质 | 核心能力 | 整体拷贝成本 | 适用场景 |
Array<T> | 固定 | 引用类型(结构体包装) | 不可增删,可修改元素 | 仅复制引用 | 元素数量固定的场景 |
VArray<T,$N> | 固定 | 值类型 | 不可增删 | 按字节全复制 | 需减少堆内存、元素类型简单场景 |
ArrayList<T> | 动态 | 引用类型 | 可增删改,支持扩容 | 仅复制引用 | 元素数量动态变化的场景 |
引用类型的数组Array
Array<T> ,其中T 表示 Array 的元素类型。引用类型(对象在堆上),放 同一种类型 T 的元素,顺序固定,长度 创建后就不可变。用来构造单一元素类型,有序序列的数据。
可以轻松使用字面量来初始化一个 Array,只需要使用方括号将逗号分隔的值列表括起来即可。如:
let numbers: Array<Int64> = [1, 2, 3, 4]
// 也可省类型:let numbers = [1, 2, 3, 4]
也可以使用构造函数的方式构造一个指定元素类型的 Array。其中,repeat 属于 Array 构造函数中的一个命名参数。如:
// 1. 指定长度 + 重复值
let zeros = Array<Int64>(10, repeat: 0) // [0,0,0,0,0,0,0,0,0,0]
// 2. 长度 + lambda 表达式
let squares = Array<Int64>(5, { i => i * i }) // [0,1,4,9,16]
需要注意的是,当通过 repeat 指定的初始值初始化 Array 时,该构造函数不会拷贝 repeat,如果 repeat 是一个引用类型,构造后数组的每一个元素都将指向相同的引用。如:
let d = Array<Int64>(3, repeat: 0) // repeat创建一个元素类型为Int64,长度为3,初始化为0
元素类型相同的 Array之间,可以互相赋值。元素类型不相同的 Array 是不相同的类型,不可以互相赋值。如:
let a: Array<Int64> = [1, 2]
let b: Array<UInt8> = [1, 2]
// a = b // ❌ 类型不匹配
可以通过索引(从 0 开始)访问如 numbers[0]。例如:
let arr = [0,1,2,3,4,5]
println("第一个元素是${arr[0]}") //第一个元素是0
arr [0] = 3
println("现在第一个元素是${arr[0]}") //现在第一个元素是3
可以使用 for-in 循环遍历 Array 的所有元素。如:
let numbers: Array<Int64> = [1, 2, 3, 4, 5]
for (i in numbers) { println(i) }
遍历元素常规写法:
for (i in 0.. numbers.size) {
println("v[${i}] = ${v[i]}")
}
简单而完整的示例:
main() {let v: Array<Int64> = [10, 20, 30, 40]// for-in 循环遍历for (i in v) {println(i)}// 遍历元素,常规写法for (i in 0..v.size) {println("v[${i}] = ${v[i]}")}
}
编译运行截图:
可以使用 size 属性获得 Array 包含的元素个数。如:
main() {
let arr = [0, 1, 2]
println("数组的大小为 ${arr.size}") // 数组的大小为 3
}
综合示例
main() {// Array示例:存储固定的3个月份let months: Array<String> = ["Jan", "Feb", "Mar"]println(months[1]) // 输出:Janmonths[1] = "February" // 允许修改元素println(months[1]) // 输出:Janlet d = Array<Int64>(3, repeat: 0) // repeat创建一个元素类型为Int64,长度为3,所有元素初始化为0的数组 for (n in d) { println(n)} var a: Array<Int64> = [0, 0, 0, 0] // 元素类型为Int64的数组var b: Array<String> = ["a1", "a2", "a3"] // 元素类型为String的数组var c: Array<String> = b //元素类型相同的 Array之间可以互相赋值println("a的元素个数${a.size}")//元素个数//for-in 遍历for (n in c) { println(n)}//编译器会根据上下文自动推断 Array 字面量的类型。var x: Array<String> =[] //创建一个元素类型为String的空数组x = ["bb1","bb2"]for (n in x) { println(n)}let y =[1,2,3] //创建元素类型为Int64的数组,包含元素1,2,3for (n in y) { println(n)}// x = y // 类型不匹配
}
输出:
Feb
February
0
0
0
a的元素个数4
a1
a2
a3
bb1
bb2
1
2
3
注意,Array 是一种长度不变的 Collection(集合) 类型,因此 Array 没有提供添加和删除元素的成员函数。
注意,Array 是一种长度不变的 Collection(集合) 类型,因此 Array 没有提供添加和删除元素的成员函数。
数组切片(返回新 Array)
从 数组 numbers 中截取(切片)出下标 1 到下标 3(左闭右开区间)的所有元素,组成一个新的 Array,然后把这个新数组赋值给常量 slice。
示例:
main() {let numbers: Array<Int64> = [10, 99, 3, 7]let slice = numbers[1..3] // [99, 3] for (n in slice) { println(n)}
}
编译运行截图:
值类型的数组VArray
VArray<T, $N>,不能省略 <T, $N>,其中 T 表示该值类型数组的元素类型(如Int64
、Float32
、Bool
等【注】),$N 是一个固定的语法。通过 $ 加上一个 Int64 类型的数值字面量表示这个值类型数组的长度( $
开头后接数字)。
【注】:由于运行时后端限制,当前 VArray<T, $N> 的元素类型 T 或 T 的成员不能包含引用类型(class 、 Array 、String等)、枚举类型、Lambda 表达式(CFunc 除外)以及未实例化的泛型类型。如:let v1: VArray<String, $3> = ["a1", "a2", "a3"] 是错误的——String 做不了 VArray 元素。
VArray 可以由一个数组的字面量来进行初始化。如:
let rgb: VArray<UInt8, $3> = [255, 128, 0]
也可以用构造函数进行初始化。其中,repeat 属于 Array 构造函数中的一个命名参数。如:
let c = VArray<Int64, $5>(repeat: 0) // 生成 [0, 0, 0, 0, 0](5 个 0)。
let b = VArray<Int64, $5>({ i => i }) // lambda 表达式,生成 [0, 1, 2, 3, 4]。
用下标[] 操作符访问和修改元素:用 [] 加索引(索引必须是整数,从 0 开始),例如:
var a: VArray<Int64, $3> = [1, 2, 3]
let second = a[1] // 取第2个元素(值为2)
a[2] = 4 // 修改第3个元素,现在数组是 [1, 2, 4]
用 size 获取 VArray 长度。例如:
var a: VArray<Int64, $3> = [1, 2, 3]
let s = a.size // 3
VArray<T, $N> 和 Array<T> 作为仓颉中两种固定长度的数组类型,基础操作有一定相似性,但也有不同,如遍历元素,目前版本(Cangjie语言首个LTS版本1.0.0)的 VArray 不支持 for-in遍历元素,可用常规写法:
main() {let v: VArray<Int64,$4> = [10, 20, 30, 40]// // 不支持for-in 循环遍历// for (i in v) {// println(i)// }// 遍历元素,常规写法for (i in 0..v.size) {println("v[${i}] = ${v[i]}")}
}
输出:
v[0] = 10
v[1] = 20
v[2] = 30
v[3] = 40
与频繁使用引用类型 Array 相比,使用值类型 VArray 可以减少堆上内存分配和垃圾回收的压力。但是需要注意的是,由于值类型本身在传递和赋值时的拷贝,会产生额外的性能开销,因此建议不要在性能敏感场景使用较大长度的 VArray。
【——如何理解仓颉编程语言官方文档这句话?
VArray 和 Array 各有性能优劣,需要根据场景选择 —— 前者能减轻内存管理压力,但拷贝成本高;后者传递成本低,但会增加内存回收负担。
1. 为什么 VArray 能 “减少堆上内存分配和垃圾回收的压力”?
内存存储位置不同:
引用类型的 Array 数据通常存在“堆”里,每次创建 Array 都要在堆里申请一块空间;而堆里的空间不会自动释放,需要 “垃圾回收器”(Garbage Collector)定期来清理不用的空间。如果频繁创建 Array,堆里会堆积大量临时空间,垃圾回收器就需要频繁工作(压力大),甚至可能影响程序运行流畅度。
而值类型的 VArray 数据通常存在“栈”里,栈的空间会随着变量的生命周期自动释放(比如函数执行完,栈上的 VArray 就自动消失),不需要垃圾回收器操心。因此,用 VArray 可以减少堆的使用,自然就减轻了垃圾回收的压力。
2. 为什么“不要在性能敏感场景使用较大长度的 VArray”?
值类型的“拷贝成本”问题:
值类型的特点是“赋值或传递时会完整拷贝数据”。比如一个长度为 1000 的 VArray,每次把它传给函数、或者赋值给另一个变量时,都要复制 1000 个元素(相当于把小盒子里的东西全倒出来,再一个个装进新盒子)。
如果 VArray 很小(比如长度 3),拷贝成本可以忽略;但如果是大长度(比如长度 10000),每次拷贝都会消耗大量时间和内存带宽。在“性能敏感场景”比如游戏的帧循环、高频数据处理)中,这种频繁的大拷贝会明显拖慢速度,反而不如用 Array(引用类型传递时只拷贝一个“地址”成本极低)。
总结:是 “内存管理压力” 和 “拷贝成本” 的权衡
小长度数组:用 VArray 更合适 —— 既减少堆内存和垃圾回收的麻烦,拷贝成本又低。
大长度数组:尤其在频繁传递 / 赋值的性能敏感场景,用 Array 更合适 —— 虽然有堆内存和垃圾回收的压力,但传递成本低,避免了大拷贝的性能损耗。
值类型的特点是 “赋值或传递时会完整拷贝数据”,引用类型的特点:赋值或传递时通常不会拷贝数据本身,而是拷贝 “引用”(即数据的内存地址)。这意味着多个引用类型变量可以共享同一份数据,修改其中一个变量指向的数据,会影响所有指向该数据的变量。
“通常不会拷贝”≠“永远不会拷贝”。
仓颉的 Array、String 等引用类型在写时复制(COW :copy-on-write) 优化下,第一次真正修改时仍可能触发一次惰性拷贝,从而把共享拆成两份数据。
因此:
• 日常代码层面:
“赋值/传参只拷引用,修改共享数据会互相可见”这句话成立。
• 底层实现细节:
如果对象内部做 COW,则真实的物理拷贝会延迟到第一次写操作。】
ArrayList 类型
这个是动态数组(也叫顺序表)。使用 ArrayList 类型需要导入 collection 包:
import std.collection.*
【ArrayList相关官方文档:https://cangjie-lang.cn/docs?url=%2F1.0.0%2Fuser_manual%2Fsource_zh_cn%2Fcollections%2Fcollection_arraylist.html 】
Array和ArrayList的异同点
Array:如果不需要增加和删除元素,但需要修改元素,就应该使用它。
ArrayList:如果需要频繁对元素增删查改,就应该使用它。相比 Array,ArrayList 既可以原地修改元素,也可以原地增加和删除元素。
使用 ArrayList 类型需要导入 collection 包:import std.collection.*
ArrayList 的可变性是一个非常有用的特征,可以让同一个 ArrayList 实例的所有引用都共享同样的元素,并且对它们统一进行修改。
不同元素类型的ArrayList是不同类型,不能互相赋值。如:
var intList: ArrayList<Int64> = ...
var strList: ArrayList<String> = ...
strList = intList // 不合法报错,类型不匹配
仓颉提供了多种构造ArrayList的方式:
// 1. 创建空的ArrayList
let emptyList = ArrayList<String>()
// 2. 指定初始容量创建
let preAllocated = ArrayList<String>(100) // 预分配100个元素的空间
// 3. 从数组初始化
let fromArray = ArrayList<Int64>([0, 1, 2])
// 4. 从其他Collection初始化
let copyList = ArrayList<Int64>(fromArray)
// 5. 通过函数规则初始化
let funcInit = ArrayList<String>(2, {x: Int64 => x.toString()})
ArrayList 的基本用法
1.使用size属性获取大小:
let list = ArrayList<Int64>([0, 1, 2])
println("list大小: ${list.size}")
2.访问单个元素:使用下标语法。示例:
main(){let list = ArrayList<Int64>([0, 1, 2])let first = list[0] // 正确,访问第一个元素let last = list[list.size - 1] // 正确,访问最后一个元素 println(first) // 0println(last) // 2
}
3. 遍历元素:使用for-in循环。示例:
main(){let list = ArrayList<Int64>([0, 1, 2])for (i in list) {println("元素: ${i}")}
}
4.范围访问:支持Range语法(与 Array 相同)。示例:
main() {// 创建一个包含 0-9 的 ArrayListlet list = ArrayList<Int64>([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])// 1. 获取从索引 2 到 5(包含 2,不包含 5)的元素let sub1 = list[2..5]println("子序列 [2..5]: ${sub1}") // 输出: [2, 3, 4]// 2. 获取从索引 0 到 3(包含 0,不包含 3)的元素let sub2 = list[0..3]println("子序列 [0..3]: ${sub2}") // 输出: [0, 1, 2]// 3. 获取从索引 6 到末尾的元素let sub3 = list[6..list.size]println("子序列 [6..end]: ${sub3}") // 输出: [6, 7, 8, 9]
}
重要特性
1.引用类型特性:
赋值时不拷贝数据,仅传递引用
所有引用共享同一数据,一处修改处处可见
示例:
main() {let list1 = ArrayList<Int64>([0, 1, 2])let list2 = list1list2[0] = 3// list1 和 list2 现在都为 [3, 1, 2] for (i in list1) {println("list1元素: ${i}")}for (i in list2) {println("list2元素: ${i}")}
}
2.ArrayList自动扩容机制:
当元素数量超过当前容量时,会自动分配更大的内存。扩容操作有性能成本。
可通过初始化时指定容量或使用reserve()方法预分配空间
示例:
import std.collection.*
import std.time.* // 用于计时MonoTime.now()main() {// 创建未指定初始容量的空ArrayList(初始容量较小,假设为10)let list = ArrayList<Int64>(10)let start = MonoTime.now() // 记录开始时间// 循环添加10000个元素,会多次触发自动扩容for (i in 0..10000) {list.add(i)}let end = MonoTime.now() // 记录结束时间println("未预分配容量时,添加10000个元素耗时: ${end - start}毫秒")
}
说明:
每次扩容都需要申请新内存并复制现有元素,多次扩容会累积性能成本,导致总耗时较长。
也可以使用reserve()方法预分配。示例:
import std.collection.*
import std.time.* // 用于计时,MonoTime.now()main() {let list = ArrayList<Int64>(10)list.reserve(10000) // 手动预分配足够容量let start = MonoTime.now()for (i in 0..10000) {list.add(i)}let end = MonoTime.now()println("reserve预分配容量时,添加10000个元素耗时: ${end - start}毫秒")
}
运行对比,后面的优化方案的耗时会显著低于“无预分配”的场景。
最后给出一个ArrayList综合示例:
import std.collection.*main() {// 1. 创建let list = ArrayList<String>() // 空列表// let mut list = ArrayList<String>(100) // 预分配 100 容量// let list = ArrayList<Int64>([0, 1, 2]) // 用 Collection 初始化// 2. 增list.add("Apple")list.add("Banana")list.add(all: ["Orange", "Pear"]) // 批量追加 [Apple Banana Orange Pear]for (item in list) {println(item)}// 3. 插list.add("Grape", at: 1) // 索引 1 处插入// 4. 删list.remove(at: 2) // 删除索引 2 的元素// 5. 改list[0] = "Pineapple"// 6. 查println("size = ${list.size}") // size = 3println("first = ${list[0]}") // first = Pineapple// 7. 遍历 [Pineapple Grape Orange Pear]for (item in list) {println(item)}
}