【仓颉语言入门 · 第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~20struct/class、构造与属性、接口、枚举与 match 模式匹配、泛型、扩展
五、工程化与标准库21~25cjpm 包管理与多文件、文件 IO、JSON 处理、网络编程、单元测试
六、并发编程26~28线程、Channel 通道与同步原语、并发实战
七、项目实战29~30命令行小工具、GeoJSON 数据处理实战
  1. 环境搭建与第一个仓颉程序
  2. 变量与常量:let / var 与基本数据类型
  3. 运算符与标准输入输出
  4. 分支结构:if 与 match 表达式
  5. 循环结构:while / for / Range
  6. 字符串详解与字符串插值
  7. 数组 Array 与区间 Range
  8. 集合框架:ArrayList、HashMap、HashSet(本文)
  9. 可空类型 ? 与 Option
  10. 错误处理:异常机制与 Result
  11. 函数定义、参数与返回值
  12. Lambda 与高阶函数
  13. 闭包、作用域与函数类型
  14. 迭代器 Iterator 与 Sequence
  15. 结构体 struct 与类 class
  16. 构造函数、属性与方法
  17. 接口 interface 与实现
  18. 枚举 enum、代数数据类型与 match 模式匹配
  19. 泛型编程
  20. 扩展、类型别名与可见性控制
  21. cjpm 包管理与多文件项目组织
  22. 文件与目录 IO
  23. JSON 处理(结合 stdx 扩展库)
  24. 网络编程入门
  25. 单元测试
  26. 并发基础:线程的创建与等待
  27. Channel 通道与同步原语
  28. 并发实战:多线程任务处理
  29. 实战一:带文件持久化的命令行小工具
  30. 实战二: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() 判空。
  • 注意 cartvar 声明?其实这里 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 个是橙子

和数组的遍历写法完全一致——ArrayListArray 都是"可迭代"的,第 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 节的引用共享,调试器里看得明明白白:

  1. let c = a.clone() 一行打断点(可以把 2.5 节的代码临时存成另一个 main);
  2. Shift + F9 启动调试,断住后在 Variables 面板展开 ab,都是 3 个元素;
  3. 单步(F8)越过 b.add(4)a 和 b 同时变成 4 个元素——因为它们本来就是同一个对象;
  4. 再越过 c.add(99):只有 c 多了 99,a 纹丝不动——clone 出来的才是复制品;
  5. 展开 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 / getget 越界返回 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 课专门讲它,你会发现这是仓颉防“空指针”的核心设计。


八、课后练习

  1. ArrayList<Int64> 从空表开始,add 进 1~10 十个数,再用循环把其中所有的偶数按下标区间逐个删掉(提示:删除后下标会前移,倒着遍历 for (i in n - 1..=0 : -1) 不容易错——第 5 课的负步长来了)。输出最终剩下的 [1, 3, 5, 7, 9]
  2. 统计单词频率:给定 let words = ["apple", "banana", "apple", "cherry", "banana", "apple"],用 HashMap<String, Int64> 统计每个单词出现的次数(已有键就 add 覆盖旧值 +1),最后遍历打印"单词 -> 次数"。
  3. 数组去重:把 ["张三", "李四", "张三", "王五", "李四"] 交给 HashSet 去重,然后数一数原始 5 个名字里有几对重复票(对照 add 返回值为 false 的次数),输出"去重后 N 人,重复 M 次"。
  4. 引用共享实验:let a = ArrayList<Int64>([1, 2, 3]);先 let b = ab.add(0),打印 a、b 观察;再 let c = a.clone()c.add(99),再次打印三者。用一句话总结哪次改动影响了 a、哪次没有。
  5. 打开 6.1 的程序 Shift + F9 调试,在 let old = phones.add(...) 一行打断点,展开 Variables 里的 phones 观察 3 个键值对;单步越过覆盖 add,确认张三的值已变成新号码且 old 显示旧号码;再展开 lucky,对照 CIDE 显示的元素个数与你的去重逻辑。

下节预告

这节课你已经在报错信息和输出里见了好几回 Some(...)NoneOption——它们是仓颉表达"可能没有值"的官方姿势。第 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
Logo

一站式 AI 云服务平台

更多推荐