【仓颉语言入门 · 第8课】
【仓颉语言入门 · 第8课】集合框架:ArrayList、HashMap 与 HashSet
上节课结尾留了个悬念:数组的长度天生不能变——想往里追加一件商品、删掉一个联系人,
Array都做不到。这节课请出仓颉标准库的集合框架三剑客:ArrayList(会伸缩的动态数组)、HashMap(按"键"查"值"的字典)、HashSet(天生不重复的集合)。学会它们,“不断添加的购物车”“姓名→电话的通讯录”"自动去重的名单"就都手到擒来了。本文是系列第 8 课:系统学习三个最常用集合的创建、增删改查、遍历,实测 ArrayList 赋值是"引用共享" 这个反直觉点,澄清旧教程里
append/put的写法陷阱,最后用"购物车 + 通讯录 + 抽奖去重"综合实战收尾。
目录(系列导航)
整套路线共 7 个模块、30 课,按每周 2~3 课的节奏,大约 2~3 个月可以走完一遍:
| 模块 | 课次 | 内容 |
|---|---|---|
| 一、环境与入门 | 01~05 | 环境搭建与 Hello World、变量与基本类型、运算符与输入输出、分支、循环 |
| 二、常用类型与数据组织 | 06~10 | 字符串、数组与区间、ArrayList/HashMap/HashSet、可空类型、错误处理 |
| 三、函数与函数式 | 11~14 | 函数、Lambda 与高阶函数、闭包、迭代器与惰性序列 |
| 四、面向对象与类型系统 | 15~20 | struct/class、构造与属性、接口、枚举与 match 模式匹配、泛型、扩展 |
| 五、工程化与标准库 | 21~25 | cjpm 包管理与多文件、文件 IO、JSON 处理、网络编程、单元测试 |
| 六、并发编程 | 26~28 | 线程、Channel 通道与同步原语、并发实战 |
| 七、项目实战 | 29~30 | 命令行小工具、GeoJSON 数据处理实战 |
- 环境搭建与第一个仓颉程序
- 变量与常量:let / var 与基本数据类型
- 运算符与标准输入输出
- 分支结构:if 与 match 表达式
- 循环结构:while / for / Range
- 字符串详解与字符串插值
- 数组 Array 与区间 Range
- 集合框架:ArrayList、HashMap、HashSet(本文)
- 可空类型
?与 Option - 错误处理:异常机制与 Result
- 函数定义、参数与返回值
- Lambda 与高阶函数
- 闭包、作用域与函数类型
- 迭代器 Iterator 与 Sequence
- 结构体 struct 与类 class
- 构造函数、属性与方法
- 接口 interface 与实现
- 枚举 enum、代数数据类型与 match 模式匹配
- 泛型编程
- 扩展、类型别名与可见性控制
- cjpm 包管理与多文件项目组织
- 文件与目录 IO
- JSON 处理(结合 stdx 扩展库)
- 网络编程入门
- 单元测试
- 并发基础:线程的创建与等待
- Channel 通道与同步原语
- 并发实战:多线程任务处理
- 实战一:带文件持久化的命令行小工具
- 实战二:GeoJSON 数据处理程序
一、为什么需要动态集合
第 7 课说过,Array 是定长的:创建时几个元素,这辈子就几个元素。但真实程序里,数据的个数往往事先不知道:
- 购物车:用户随时可能"加入购物车",也可能中途移除;
- 聊天记录:每来一条消息就得多存一条;
- 录入成绩:要录多少个学生,得问完用户才知道。
把第 7 课的报错再回忆一下——a.append(4) 会直接编译失败,因为 Array 根本没提供增删能力。仓颉把这些能力放进了 std.collection 包的三个集合类型里,它们都在标准库的 std.collection 包中,使用前先导入:
import std.collection.*
三剑客的分工一句话说清:
| 类型 | 管什么 | 现实比喻 |
|---|---|---|
ArrayList<T> | 按顺序排队的一串数据 | 排队买奶茶的队伍 |
HashMap<K, V> | 按"键"快速找"值" | 通讯录:名字→号码 |
HashSet<T> | 只留不重复的元素 | 抽奖名单(一人一票) |
二、ArrayList:会伸缩的数组
2.1 创建:用已有的数组"升级"最方便
ArrayList 的类型写作 ArrayList<T>(T 是元素类型)。最常用的创建方式是把一个数组交给它——正好接住第 6 课 split、第 7 课数组字面量的家底:
package helloCangjie
import std.collection.*
main(): Int64 {
// 方式①:把数组整个交给 ArrayList
let list = ArrayList<Int64>([1, 2, 3])
println("数组构造:${list}")
// 方式②:指定个数 + 初始化函数(生成 0, 10, 20)
let gen = ArrayList<Int64>(3, {i => i * 10})
println("初始化函数:${gen}")
// 方式③:空表(元素个数从 0 开始涨)
let empty = ArrayList<String>()
println("空表 isEmpty:${empty.isEmpty()} size:${empty.size}")
return 0
}
运行结果:
数组构造:[1, 2, 3]
初始化函数:[0, 10, 20]
空表 isEmpty:true size:0
方式②的 {i => i * 10} 是"给下标算元素"的小函数,现在照着写就行,第 12 课 Lambda 会专门讲它。
⚠️ 你可能在别的语言见过
ArrayList<Int64>[1, 2, 3]这种方括号字面量写法——仓颉 SDK(1.1.3)编译它直接报错:'[]' is non-static member, cannot access by type name。请统一用圆括号传数组:ArrayList<Int64>([1, 2, 3])。
2.2 增:add 往尾部追加
package helloCangjie
import std.collection.*
main(): Int64 {
var cart = ArrayList<String>(["牛奶"])
cart.add("面包")
cart.add("鸡蛋")
println("add 后:${cart} size=${cart.size}")
println("isEmpty:${cart.isEmpty()}")
return 0
}
运行结果:
add 后:[牛奶, 面包, 鸡蛋] size=3
isEmpty:false
add(元素):追加到末尾,调用多少次就涨多长;size随时读当前个数;isEmpty()判空。- 注意
cart用var声明?其实这里let也可以——ArrayList 本身可以增删改,let只限制"这个名字不许换成另一个 ArrayList"(和第 7 课数组的规则一致)。
⚠️ 旧教程里的
cart.append("面包")在 SDK 1.1.3 上编译报错:'append' is not a member ...。仓颉后来把"尾部追加"统一改名成了add,ArrayList、HashMap、HashSet 三家都用这个名字,好记。
2.3 删:remove 按"区间"删
ArrayList 的删除参数是一个区间(第 7 课的 Range 又见面了),区间是左闭右开:
package helloCangjie
import std.collection.*
main(): Int64 {
let nums = ArrayList<Int64>([10, 20, 30, 40, 50])
nums.remove(0..2) // 删下标 0、1 两个元素(2 不含)
println("删 0..2 后:${nums}")
let cart = ArrayList<String>(["牛奶", "面包", "鸡蛋"])
cart.remove(1..2) // 删单个元素:下标 i 就写 i..i+1
println("删 1..2 后:${cart}")
return 0
}
运行结果:
删 0..2 后:[30, 40, 50]
删 1..2 后:[牛奶, 鸡蛋]
- 删一段:
remove(a..b)删掉下标 a 到 b-1 的所有元素; - 删单个:
remove(i..i+1)——初学容易别扭,记住口诀"删谁就写谁的下一格"; - 还有一个
removeIf(条件函数)能"满足条件的全删",要等第 12 课学了 Lambda 才好用,这里先埋个伏笔。
2.4 改与查:下标、get、first/last、contains、slice
package helloCangjie
import std.collection.*
main(): Int64 {
let cart = ArrayList<String>(["牛奶", "面包", "鸡蛋"])
// 下标读写:和数组一模一样
println("第 1 件:${cart[0]}")
cart[1] = "可颂"
println("改后:${cart}")
// get 返回 Option:找到 Some(值),越界 None(不抛异常)
println("get(0):${cart.get(0)} get(99):${cart.get(99)}")
// first / last 是属性,不加括号
println("first:${cart.first} last:${cart.last}")
// 在不在、切一段
println("contains(鸡蛋):${cart.contains("鸡蛋")}")
println("slice(1..3):${cart.slice(1..3)}")
return 0
}
运行结果:
第 1 件:牛奶
改后:[牛奶, 可颂, 鸡蛋]
get(0):Some(牛奶) get(99):None
first:Some(牛奶) last:Some(鸡蛋)
contains(鸡蛋):true
slice(1..3):[可颂, 鸡蛋]
- 下标
[]:快,但越界直接抛异常(和数组同款防线,见 FAQ Q4); get(i):越界返回None不炸程序,返回的Some(...)/None就是第 9 课的Option,本课先记住"None代表没有";first/last是属性不是方法——别画蛇添足加括号,空表取它们得到None;slice(a..b)切出一段新的 ArrayList,改动互不影响(这点比数组的切片省心,数组切片是共享存储的)。
2.5 反直觉实测:赋值是"引用共享",独立副本要 clone
第 7 课说过数组赋值共享底层存储。ArrayList 更"彻底"——它整个是引用类型,赋值后两个名字指向同一个对象:
package helloCangjie
import std.collection.*
main(): Int64 {
let a = ArrayList<Int64>([1, 2, 3])
let b = a // 没有复制!a、b 是同一个东西的两个名字
b.add(4) // 通过 b 追加……
println("a=${a}")
println("b=${b}") // a 也跟着变了
let c = a.clone() // 真正的独立副本
c.add(99)
println("a=${a}")
println("c=${c}") // c 爱怎么改,a 都不管
return 0
}
运行结果:
a=[1, 2, 3, 4]
b=[1, 2, 3, 4]
a=[1, 2, 3, 4]
c=[1, 2, 3, 4, 99]
要独立副本,唯一姿势是 clone()。把大集合传给别人用、或者存起来当"备份"之前,先想清楚要的是"同一个"还是"复制品"。
2.6 遍历:for-in 和 enumerate
package helloCangjie
import std.collection.*
main(): Int64 {
let fruits = ArrayList<String>(["苹果", "香蕉", "橙子"])
for (f in fruits) {
println("水果:${f}")
}
for ((i, f) in fruits.enumerate()) {
println("第 ${i} 个是 ${f}")
}
return 0
}
运行结果:
水果:苹果
水果:香蕉
水果:橙子
第 0 个是苹果
第 1 个是香蕉
第 2 个是橙子
和数组的遍历写法完全一致——ArrayList 和 Array 都是"可迭代"的,第 5 课的 for-in 直接通用。
ArrayList 速查表:
| 需求 | 写法 | 备注 |
|---|---|---|
| 创建 | ArrayList<Int64>([1, 2, 3]) | 圆括号传数组 |
| 追加 | a.add(x) | 尾部,不是 append |
| 删一段 | a.remove(1..3) | 左闭右开 |
| 删单个 | a.remove(i..i+1) | 口诀"删谁写到下一格" |
| 读 / 改元素 | a[0] / a[0] = 5 | 越界抛异常 |
| 安全读 | a.get(0) | Some(v) / None |
| 首 / 尾 | a.first / a.last | 属性,不加括号 |
| 在不在 | a.contains(x) | 返回 Bool |
| 切一段 | a.slice(1..3) | 新列表,独立 |
| 个数 / 判空 | a.size / a.isEmpty() | — |
| 清空 | a.clear() | 变回空表 |
| 独立副本 | a.clone() | 引用类型必用 |
| 遍历 | for (x in a) / a.enumerate() | 与数组一致 |
三、HashMap:按名字找东西的字典
数组和 ArrayList 都是"按下标找",但现实里我们更常按名字找:按学号查成绩、按工号查员工、按单词查释义。HashMap<K, V> 就是"键值对"字典——K 是键的类型,V 是值的类型。
3.1 创建与 add:一对一对往里放
package helloCangjie
import std.collection.*
main(): Int64 {
// 方式①:把"键值对元组数组"交给它
let phones = HashMap<String, String>([
("张三", "138-0000-0001"),
("李四", "138-0000-0002")
])
println("构造:${phones}")
// 方式②:空字典,逐对 add
let dict = HashMap<String, Int64>()
dict.add("apple", 3)
dict.add("banana", 5)
println("size:${dict.size}")
return 0
}
运行结果:
构造:[(张三, 138-0000-0001), (李四, 138-0000-0002)]
size:2
("张三", "138-0000-0001") 这种用圆括号包起来的数据叫元组(Tuple),(键, 值) 成对出现。add 同一个键会覆盖旧值,而且把旧值还给你:
let old = dict.add("apple", 10) // apple 已经是 3,现在改成 10
println("${old}") // 输出 Some(3)——旧值 3 被包在 Some 里还给你
println("${dict["apple"]}") // 10
⚠️ 旧教程里的
dict.put("apple", 3)在 SDK 1.1.3 上编译报错:'put' is not a member of class 'HashMap<...>'。添加键值对统一用add。
3.2 读:下标快但要防炸,get 稳
package helloCangjie
import std.collection.*
main(): Int64 {
let dict = HashMap<String, Int64>()
dict.add("apple", 3)
dict.add("banana", 5)
// 下标读:键不存在会抛异常!
println("apple = ${dict["apple"]}")
// get 读:不存在给 None,不炸
println("get(apple):${dict.get("apple")}")
println("get(cherry):${dict.get("cherry")}")
// contains:先问一句"有没有"
println("contains(banana):${dict.contains("banana")}")
println("contains(cherry):${dict.contains("cherry")}")
return 0
}
运行结果:
apple = 3
get(apple):Some(3)
get(cherry):None
contains(banana):true
contains(cherry):false
键不确定存在时,永远先 contains 或用 get。直接 dict["cherry"] 读一个不存在的键,程序当场抛异常退出:
NoneValueException: Value does not exist!
(实测过的真实报错——"确定存在才敢用下标"是 HashMap 的安全守则。)
3.3 删:remove 连键带值一起拿走
let gone = dict.remove("banana")
println("${gone}") // Some(5)——被删的值
println("${dict.size}") // 1
remove(键) 删掉整对键值,返回值把"被删的值"包在 Some 里还给你;删不存在的键返回 None(不报错,放心删)。
3.4 遍历:三种姿势
package helloCangjie
import std.collection.*
main(): Int64 {
let dict = HashMap<String, Int64>()
dict.add("apple", 3)
dict.add("banana", 5)
dict.add("cherry", 7)
// 姿势①:键值一起拿(最常用)
for ((k, v) in dict) {
println("${k} -> ${v}")
}
// 姿势②:只要键
for (k in dict.keys()) {
println("键:${k}")
}
// 姿势③:只要值
for (v in dict.values()) {
println("值:${v}")
}
return 0
}
运行结果(你的输出顺序可能不同!):
apple -> 3
banana -> 5
cherry -> 7
键:apple
键:banana
键:cherry
值:3
值:5
值:7
⚠️ HashMap 不保证顺序。它是靠"哈希"来换取"按键秒查"的,元素存放在哪个格子由键的哈希值决定,和插入顺序无关。本例三个元素恰好按顺序显示是巧合——元素一多就会"乱序"。需要有序的场景,第 15 课之后再认识
TreeMap(排好序的字典)或OrderedMap。
四、HashSet:天生不重复的集合
HashSet<T> 只关心"有还是没有":同一个元素塞多少次,集合里只有一份。
4.1 add 返回 Bool:塞进去就知道是不是新的
package helloCangjie
import std.collection.*
main(): Int64 {
let nums = HashSet<Int64>([3, 1, 2])
println("add(3) 已存在,返回:${nums.add(3)}")
println("add(4) 是新的,返回:${nums.add(4)}")
println("contains(2):${nums.contains(2)}")
println("remove(1):${nums.remove(1)}")
println("集合:${nums} size:${nums.size}")
return 0
}
运行结果(集合显示顺序可能不同):
add(3) 已存在,返回:false
add(4) 是新的,返回:true
contains(2):true
remove(1):true
集合:[3, 2, 4] size:3
三个值得玩味的细节:
add返回Bool:加成功了(原本没有)返回true,元素已存在返回false——一行代码同时完成"去重 + 判断是否重复";- 显示顺序
[3, 2, 4]和插入顺序无关:和 HashMap 一样按哈希存放,不保证顺序; remove(元素)返回 Bool:删掉了(原本有)true,没有这个元素false。
4.2 最经典的应用:数组去重
第 7 课的遗留难题——“把数组里的重复元素去掉”,HashSet 一行就解决:
let raw = [1, 3, 2, 3, 1, 5] // Array<Int64>,有重复
let unique = HashSet<Int64>(raw) // 交给 HashSet,重复自动消失
for (x in unique) {
print("${x} ")
}
println()
HashSet 速查表:
| 需求 | 写法 | 备注 |
|---|---|---|
| 创建 | HashSet<String>(["a", "b"]) | 圆括号传数组 |
| 添加 | s.add(x) | 返回 Bool:true=新加 |
| 在不在 | s.contains(x) | HashSet 的主业 |
| 删除 | s.remove(x) | 返回 Bool |
| 个数 / 清空 | s.size / s.clear() | — |
| 独立副本 | s.clone() | 引用类型 |
| 遍历 | for (x in s) | 顺序不保证 |
五、三剑客怎么选
对着需求挑就行:
| 场景 | 选谁 | 理由 |
|---|---|---|
| 一串有序数据,个数会变 | ArrayList<T> | 顺序保留,随便增删 |
| 按"名字"查"东西" | HashMap<K, V> | 键值对,按键秒查 |
| 只要"出现过没有" / 去重 | HashSet<T> | 天生不重复,判断 O(1) |
| 个数固定不变 | Array<T>(第 7 课) | 定长但更轻量 |
三个集合都是引用类型:赋值、传参共享同一个对象,要独立副本一律 clone()。
六、CIDE 实操:购物车 + 通讯录 + 抽奖去重(综合实战)
综合本课知识:用 ArrayList 管"会增删的购物车",用 HashMap 管"按名字查的通讯录",用 HashSet 做"投票去重",三个场景覆盖三剑客的全部高频 API。
6.1 编写程序
继续使用 helloCangjie 项目,把 src/main.cj 替换为:
package helloCangjie
import std.collection.*
main(): Int64 {
// ① 购物车:ArrayList 动态增删(数组做不到的"追加"在这里随便加)
let cart = ArrayList<String>()
cart.add("牛奶")
cart.add("面包")
cart.add("鸡蛋")
cart.add("可乐")
println("------ 我的购物车 ------")
for ((i, item) in cart.enumerate()) {
println("${i + 1}. ${item}")
}
println("共 ${cart.size} 件")
// ② 反悔了:找到"面包"的位置并移除(remove 参数是左闭右开区间,删单个写 i..i+1)
var pos: Int64 = -1
for ((i, item) in cart.enumerate()) {
if (item == "面包") {
pos = i
}
}
if (pos >= 0) {
cart.remove(pos..pos + 1)
println("已移除面包,剩 ${cart.size} 件:${cart}")
}
// ③ 通讯录:HashMap 按名字查电话
let phones = HashMap<String, String>()
phones.add("张三", "138-0000-0001")
phones.add("李四", "138-0000-0002")
phones.add("王五", "138-0000-0003")
println("------ 通讯录(${phones.size} 位联系人)------")
for ((name, num) in phones) {
println("${name}:${num}")
}
// ④ 查号:contains 判断在不在,在才用下标读(第 9 课的 Option 是更优雅的姿势)
let who = "李四"
if (phones.contains(who)) {
println("${who} 的号码:${phones[who]}")
} else {
println("查无此人:${who}")
}
// ⑤ 张三换号了:再 add 同一个键就是覆盖,返回值是被覆盖的旧号码
let old = phones.add("张三", "139-9999-9999")
println("张三的旧号码:${old},新号码:${phones["张三"]}")
// ⑥ 王五搬走了:remove 删掉整对键值,返回被删的值
println("删除王五:${phones.remove("王五")},剩 ${phones.size} 位联系人")
// ⑦ 抽奖去重:HashSet 天生不重复,add 返回 false 说明是重复票
let tickets = ["小明", "小红", "小明", "小刚", "小红", "小明"]
let lucky = HashSet<String>()
var dup = 0
for (name in tickets) {
if (!lucky.add(name)) {
dup += 1
}
}
println("------ 抽奖去重 ------")
println("收到 ${tickets.size} 张票,去重后 ${lucky.size} 位候选人,废弃重复票 ${dup} 张")
print("候选人:")
for (name in lucky) {
print("${name} ")
}
println()
return 0
}
设计要点:
- 购物车找位置用"打标记":
pos先放-1(没找到),遍历中命中就记下标——这是"先查后删"的经典套路; - 删单个元素
remove(pos..pos + 1):区间写法再别扭也比"新建一个列表再拷贝回去"省事; - 通讯录查号走"先 contains 再下标":避开"键不存在抛
NoneValueException"的坑; - 覆盖 add 拿旧值:
phones.add("张三", 新号)返回Some(旧号),换号场景顺便验证了"add 同键即覆盖"; - 去重统计一石二鸟:
!lucky.add(name)为 true 时说明是重复票,dup顺手 +1。
6.2 运行与验证
按 Ctrl + R 运行:
------ 我的购物车 ------
1. 牛奶
2. 面包
3. 鸡蛋
4. 可乐
共 4 件
已移除面包,剩 3 件:[牛奶, 鸡蛋, 可乐]
------ 通讯录(3 位联系人)------
张三:138-0000-0001
李四:138-0000-0002
王五:138-0000-0003
李四 的号码:138-0000-0002
张三的旧号码:Some(138-0000-0001),新号码:139-9999-9999
删除王五:Some(138-0000-0003),剩 2 位联系人
------ 抽奖去重 ------
收到 6 张票,去重后 3 位候选人,废弃重复票 3 张
候选人:小明 小红 小刚
(通讯录和"候选人"两段的显示顺序由哈希决定,你的输出顺序可能不同——内容一致即可,这正是 3.4 节说的"HashMap/HashSet 不保证顺序"。)
可以自己动手改:
- 给购物车加一件"泡面"再移除它,观察
size变化; - 把 ④ 的
who改成"赵六"(不存在的联系人),走else分支看"查无此人"; - 在 ⑦ 之前打印一次
lucky.size,体会 HashSet 从 0 涨到 3 的过程; - 故意写
phones["赵六"]读不存在的键,看看NoneValueException: Value does not exist!长什么样(改回来就行,亲眼看一次比背十遍管用)。
📸 配图建议:Result 面板完整显示"购物车 / 通讯录 / 抽奖去重"三段报告,能看到移除面包后的 3 件商品和 Some(…) 包裹的旧号码。
6.3 用调试器看清"同一个对象还是复制品"
2.5 节的引用共享,调试器里看得明明白白:
- 在
let c = a.clone()一行打断点(可以把 2.5 节的代码临时存成另一个 main); - Shift + F9 启动调试,断住后在 Variables 面板展开
a和b,都是 3 个元素; - 单步(F8)越过
b.add(4):a 和 b 同时变成 4 个元素——因为它们本来就是同一个对象; - 再越过
c.add(99):只有c多了 99,a纹丝不动——clone 出来的才是复制品; - 展开
lucky(HashSet)这类集合,CIDE 会直接显示元素个数(如[3 项]),点开箭头逐个看元素。
📸 配图建议:断点命中时 Variables 面板中 a、b 元素一致,c 多出 99 的对比截图。
调试结束点停止按钮。
七、常见问题 FAQ
Q1:教程/同事写的 list.append(x)、map.put(k, v) 为什么编译报错?
旧写法已被新版标准库移除:append 换成了 add(ArrayList 追加),put 换成了 add(HashMap 添加),报错形如 'put' is not a member of class 'HashMap<...>'。三剑客的"加东西"现在统一叫 add,本课所有示例均按新写法。
Q2:ArrayList<Int64>[1, 2, 3] 这种方括号写法为什么报 '[]' is non-static member, cannot access by type name?
新版标准库不支持对类型名直接用方括号字面量构造。请写 ArrayList<Int64>([1, 2, 3])——把数组用圆括号传进去。
Q3:HashMap / HashSet 打印出来顺序是乱的,是 bug 吗?
不是。两者按哈希值决定存放位置,不保证顺序(本课多处实测顺序与插入序不同)。要"保持插入顺序"或"按键排序",分别用 OrderedMap / TreeMap,后续课程遇到再讲。
Q4:dict[不存在的键]、list[越界下标] 会怎样?
运行时抛异常:HashMap 是 NoneValueException: Value does not exist!,ArrayList 与数组同款 IndexOutOfBoundsException。拿不准就先 contains / get:get 越界返回 None 不炸程序,第 9 课学 Option 后会是你最顺手的工具。
Q5:ArrayList 删一个元素为什么写 remove(i..i+1) 这么怪?
新版标准库的 remove 接收区间(一次删一段),没有"按下标删单个"的重载。左闭右开区间删单个元素就是 i..i+1,配合注释不难读。想"按条件批量删"用 removeIf,等第 12 课学了 Lambda 再回头用它。
Q6:dict.keys() 能直接放进 println 打印吗?
不能,keys() / values() 返回的是"可迭代视图"而不是字符串,直接插值打印编译报错(提示不实现 ToString)。用 for (k in dict.keys()) 逐个处理,或打印整个 dict。
Q7:let b = a 之后往 b 里 add,为什么 a 也变了?
ArrayList / HashMap / HashSet 是引用类型:赋值只复制"地址",两个名字指向同一个对象(2.5 节实测)。这与第 7 课数组的"共享底层存储"表现类似但机制不同——数组是值类型、整体赋值不换对象;集合要独立副本一律 clone()。
Q8:get / first / last / remove 返回的 Some(...)、None 到底是什么?
Option 类型:有结果就 Some(值) 包一层,没有就是 None。本课先记住两条:“None 代表没有”、“需要真正的值时用 getOrThrow()”(本课实测过它取回包裹的值)。第 9 课专门讲它,你会发现这是仓颉防“空指针”的核心设计。
八、课后练习
- 用
ArrayList<Int64>从空表开始,add进 1~10 十个数,再用循环把其中所有的偶数按下标区间逐个删掉(提示:删除后下标会前移,倒着遍历for (i in n - 1..=0 : -1)不容易错——第 5 课的负步长来了)。输出最终剩下的[1, 3, 5, 7, 9]。 - 统计单词频率:给定
let words = ["apple", "banana", "apple", "cherry", "banana", "apple"],用HashMap<String, Int64>统计每个单词出现的次数(已有键就add覆盖旧值 +1),最后遍历打印"单词 -> 次数"。 - 数组去重:把
["张三", "李四", "张三", "王五", "李四"]交给HashSet去重,然后数一数原始 5 个名字里有几对重复票(对照add返回值为 false 的次数),输出"去重后 N 人,重复 M 次"。 - 引用共享实验:
let a = ArrayList<Int64>([1, 2, 3]);先let b = a后b.add(0),打印 a、b 观察;再let c = a.clone()后c.add(99),再次打印三者。用一句话总结哪次改动影响了a、哪次没有。 - 打开 6.1 的程序 Shift + F9 调试,在
let old = phones.add(...)一行打断点,展开 Variables 里的phones观察 3 个键值对;单步越过覆盖 add,确认张三的值已变成新号码且old显示旧号码;再展开lucky,对照 CIDE 显示的元素个数与你的去重逻辑。
下节预告
这节课你已经在报错信息和输出里见了好几回 Some(...)、None、Option——它们是仓颉表达"可能没有值"的官方姿势。第 9 课正式学习可空类型 ? 与 Option:为什么仓颉敢说"没有空指针异常",isSome() / isNone() 怎么用,以及把 ? 挂在类型后面能省掉多少判断。我们下节课见!
系列说明:本系列基于 Windows 平台 + CIDE + 仓颉 SDK(1.1.3)编写,所有代码均已实际编译运行通过。如遇 SDK 版本差异导致的细节出入,以你本地版本为准,欢迎评论区交流。
� 遇到问题?扫码联系作者
跟着课程练习时,如果在 SDK 安装、环境变量配置、编译报错或调试上卡住,欢迎扫码加作者企业微信直接咨询(请备注"仓颉课程"):

离线环境下图片可能加载不出来,也可以在 CIDE 菜单 Help ▸ 联系作者 / Contact 中查看同一张二维码(应用内置兜底图,无需联网)。
�📥 工具下载
本系列全程使用的仓颉 IDE —— CIDE(免费开源、社区版):
- GitCode 仓库 / 安装包下载:https://gitcode.com/wp_upala/cide
- 打开页面后进入 发行版(Releases),两种包任选其一:
- 安装版:下载
CIDE-<版本>-x64-Setup.exe,双击安装,适合日常长期使用; - 免安装版(Portable):下载
CIDE-<版本>-x64-Portable.zip,解压到任意目录即用,不写注册表、不留安装痕迹,拷到 U 盘也能在别的电脑直接运行(包内附《使用说明.txt》)。适合先试用、或在受限电脑上学习本系列课程。
- 安装版:下载
- 仓颉 SDK 请前往仓颉编程语言官网下载:https://cangjie-lang.cn
更多推荐




所有评论(0)