倉頡編程語言青少年基礎教程:數組類型
數組本質上是有序、同類型數據的集合容器,其核心作用是高效組織、訪問和處理批量數據,同時結合語言特性,為開發者提供簡潔、高性能的數據管理方式。例如:
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)}
}