【仓颉语言入门 · 第18课】
【仓颉语言入门 · 第18课】枚举 enum、代数数据类型与 match 模式匹配
第 17 课结尾留了一个问题:一个值"要么是成功结果,要么是错误原因"“要么是圆,要么是矩形,要么是三角形”,这种封闭的多选一类型,用 class 继承写起来又重又不安全。本课的主角 enum(枚举) 正是答案——仓颉的枚举不是其他语言里"给整数起名字"的弱枚举,而是函数式语言里的代数数据类型(ADT):构造器可以携带不同类型、不同个数的数据,再配合
match的穷尽匹配,让编译器帮你保证"每一种可能都被处理了"。本文所有代码与报错文案均在仓颉 SDK 1.2.0 下逐行实测编译运行。
目录(系列导航)
整套路线共 7 个模块、30 课:
| 模块 | 课次 | 内容 |
|---|---|---|
| 一、环境与入门 | 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 数据处理程序
一、为什么需要枚举:从"魔法数字"说起
初学编程时,表示"春夏秋冬"你可能会这样写:
// 约定:1=春 2=夏 3=秋 4=冬
let season = 2
问题在于这个约定只存在于注释里,编译器完全不知道 1、2、3、4 是合法值。于是 let season = 99 也能编译通过,传错了、拼错了都要等运行时才暴露。
枚举把"只能取这几个值"从注释变成编译器强制的约束:
enum Season {
| Spring | Summer | Autumn | Winter
}
Season 类型的变量,永远只能是这四个值之一。花括号里列出的每一个可能取值叫做一个构造器(constructor),多个构造器之间用 | 分隔(第一个 | 可写可不写)。
注意:这里的"构造器"和 class 里的构造函数
init不是一回事,它指的是枚举类型的一个可能取值。
二、定义和使用枚举
2.1 创建枚举值
用 类型名.构造器 创建一个枚举值;当没有同名变量/函数冲突时,也可以直接写构造器名:
enum Season {
| Spring | Summer | Autumn | Winter
}
func describe(s: Season): String {
match (s) {
case Spring => "春"
case Summer => "夏"
case Autumn => "秋"
case Winter => "冬"
}
}
main(): Int64 {
let s1 = Season.Summer // 完整写法,最稳妥
let s2: Season = Summer // 简写:类型明确且无歧义时,可省略类型名
println(describe(s1)) // 夏
println(describe(s2)) // 夏——两种写法得到的是同一个枚举值
return 0
}
注意:仓颉的枚举默认不支持
==运算符(仓颉也没有===,连写三个等号会直接报语法错误),判断枚举值的标准方式是match;确实需要==时请实现Equatable接口,见 6.4 节。
2.2 枚举里可以定义方法
枚举体里除了构造器,还能定义成员函数。方法内部用 match (this) 判断"我现在是哪个构造器":
enum RGBColor {
| Red | Green
| Blue(UInt8) // 有参构造器:携带一个"亮度",下一节细讲
public func describe(): String {
match (this) {
case Red => "红色"
case Green => "绿色"
case Blue(b) => "蓝色(亮度=${b})"
}
}
}
main(): Int64 {
println(Red.describe()) // 红色
println(Blue(150).describe()) // 蓝色(亮度=150)
return 0
}
2.3 枚举只能定义在顶层
枚举只能写在源文件的顶层作用域,不能嵌套在函数、class 或其他枚举里。需要被多个文件共用时,和其他顶层声明一样放在包里即可。
三、match 与枚举:穷尽匹配
第 4 课已经用过 match 做分支,这一节把它和枚举配对的完整能力讲透。
3.1 match 是表达式,有返回值
和 if 一样,match 是表达式,可以直接算出一个值赋给变量:
main(): Int64 {
let s = Season.Summer
let text = match (s) {
case Spring => "春"
case Summer => "夏"
case Autumn => "秋"
case Winter => "冬"
}
println(text) // 夏
return 0
}
3.2 穷尽性:漏一个分支,编译就报错
match 最值钱的特性是穷尽检查(exhaustive):所有构造器必须都被覆盖。故意漏掉 Winter:
error: non-exhaustive patterns
# note: Winter is not covered
将来你给枚举新增一个构造器,所有没处理它的 match 会集体编译报错——这相当于编译器帮你把"所有需要同步修改的地方"一个个揪出来,杜绝"加了新状态却忘了处理"的线上事故。
3.3 通配符 _:我不关心具体是谁
不想逐一列出时,最后一个分支用 _ 兜底,它能匹配任何值:
match (s) {
case Summer => "放暑假"
case _ => "正常上学" // 春/秋/冬 都走这里
}
_ 还能用在有参构造器上,表示"这个参数我不要":case Blue(_) => ...。
3.4 一条 case 合并多个模式
相邻分支行为相同时,用 | 在一条 case 里合并:
enum LogLevel {
| Debug | Info | Warn | Error | Fatal
}
func needAlert(level: LogLevel): Bool {
match (level) {
case Error | Fatal => true // 错误和致命错误都要告警
case _ => false
}
}
小提示:构造器名
Error恰好与标准库的异常基类Error同名。在case里有明确的类型上下文,不受影响;但在普通表达式中直接写Error(如needAlert(Error))可能被解析成标准库类型而报错,这时写全名LogLevel.Error即可。
3.5 没有匹配值的 match:更清爽的 if-else 链
match 后面可以不跟值,此时每个 case 后面直接写一个 Bool 表达式,相当于自上而下的 if-else 链:
let score = 85
let grade = match {
case score >= 90 => "优"
case score >= 60 => "及格"
case _ => "不及格"
}
println("等级:${grade}") // 等级:及格
3.6 只关心一种情况:if-let
满屏分支有时只是为了取一个值。if-let 在"只关心某一种构造器"时比 match 简洁,注意箭头是 <-:
enum MyOpt {
| SomeS(String)
| NoneS
}
main(): Int64 {
let o: MyOpt = SomeS("hello")
if (let SomeS(text) <- o) {
println("拿到字符串:${text}") // 拿到字符串:hello
} else {
println("是空的")
}
return 0
}
标准库 Option(第 9 课)同样可以这样写:if (let Some(v) <- opt) { ... }。第 9 课我们用的是 isSome() + getOrThrow() 的姿势,if-let 是它更简洁的替代——实测在 SDK 1.2.0 下两种写法都可用。
四、有参构造器:关联值(ADT 的核心)
枚举真正强大的地方,是不同构造器可以携带不同类型、不同个数的数据,这叫关联值(associated value)。
4.1 同一个枚举,每种形态带不同的数据
用一个 Shape 枚举表示三种图形:圆只需要半径,矩形需要宽和高,点什么都不需要:
enum Shape {
| Circle(Float64) // 半径
| Rectangle(Float64, Float64) // 宽、高
| Point
}
func area(s: Shape): Float64 {
match (s) {
case Circle(r) => 3.14159265 * r * r
case Rectangle(w, h) => w * h
case Point => 0.0
}
}
main(): Int64 {
println("${area(Circle(2.0))}") // 12.566371
println("${area(Rectangle(3.0, 4.0))}") // 12.000000
println("${area(Point)}") // 0.000000
return 0
}
case Circle(r) 中的 r 是绑定模式:匹配成功时,构造器携带的数据被绑定到变量 r,分支体内直接使用。
对比用 class 实现:得写 Shape 父类 + Circle/Rectangle/Point 三个子类 + 三个文件的仪式感。而枚举把"一组封闭的形态"压缩进一个类型,数据和分支都集中可见。
4.2 同名构造器:靠参数个数区分
仓颉允许一个枚举里出现同名构造器,只要参数个数不同(无参算 0 个):
enum MyColor {
| White | Black
| White(UInt8) | Black(UInt8)
}
match (c) {
case White => println("无参 White")
case White(v) => println("带亮度 ${v} 的 White")
case _ => println("其他")
}
case White 匹配无参构造器,case White(v) 匹配有参构造器,编译器靠参数个数区分。
4.3 守卫 where:匹配成功后再加条件
模式后面可以跟 where 条件(注意不是 if),模式匹配上且条件为 true 时分支才命中。case 自上而下,第一个命中的胜出:
func describe(s: Shape): String {
match (s) {
case Circle(r) where r > 10.0 => "大圆"
case Circle(_) => "小圆" // 半径 <=10 的圆落到这里
case Rectangle(w, h) where w == h => "正方形"
case Rectangle(_, _) => "长方形"
case Point => "一个点"
}
}
实测输出:describe(Circle(20.0)) 是 大圆,describe(Rectangle(5.0, 5.0)) 是 正方形。
4.4 嵌套枚举:一层套一层地解构
构造器的参数类型可以是另一个枚举,match 时可以一层套一层地解构:
enum PayInfo {
| Unpaid
| Paid(Int64) // 支付金额(分)
}
enum OrderStatus {
| Created
| Pay(PayInfo) // 支付中,里面再带一个支付信息
| Closed
}
func statusText(s: OrderStatus): String {
match (s) {
case Created => "待支付"
case Pay(Unpaid) => "支付处理中"
case Pay(Paid(amount)) => "已支付 ${amount} 分" // 两层一次性拆开
case Closed => "已关闭"
}
}
Pay(Paid(9900)) 直接在一条 case 里拆出最里层的 amount,输出 已支付 9900 分。
五、递归枚举:表达式求值器
构造器的参数类型还可以是枚举自己,这叫递归枚举。经典例子是算术表达式:一个表达式要么是数字,要么是两个子表达式相加/相减/相乘:
enum Expr {
| Num(Int64)
| Add(Expr, Expr)
| Sub(Expr, Expr)
| Mul(Expr, Expr)
}
func eval(e: Expr): Int64 {
match (e) {
case Num(n) => n
case Add(a, b) => eval(a) + eval(b)
case Sub(a, b) => eval(a) - eval(b)
case Mul(a, b) => eval(a) * eval(b)
}
}
(1 + 2) * (10 - 3) 用嵌套构造器写出来,再交给递归的 eval:
main(): Int64 {
let e = Mul(Add(Num(1), Num(2)), Sub(Num(10), Num(3)))
println("(1+2)*(10-3) = ${eval(e)}") // (1+2)*(10-3) = 21
return 0
}
这就是 JSON 解析器、AST 抽象语法树、状态机等数据结构的标准写法——数据用递归枚举描述,处理用递归 match 完成,而且编译器保证每一种节点都被处理。
六、泛型枚举:你其实早就认识两个
6.1 亲手写一个 Option
第 9 课的 Option<T>,本质上就是一个带泛型参数的枚举:
enum MyOption<T> {
| MySome(T)
| MyNone
func unwrapOr(def: T): T {
match (this) {
case MySome(v) => v
case MyNone => def
}
}
}
main(): Int64 {
let a: MyOption<Int64> = MySome(42)
let n: MyOption<Int64> = MyNone
println("${a.unwrapOr(0)}, ${n.unwrapOr(0)}") // 42, 0
return 0
}
标准库的 enum Option<T> { Some(T) | None } 与此一模一样,只是多了 ??、?.、getOrThrow 等语法糖(第 9 课)。
6.2 亲手写一个 Result
第 10 课我们实测过:当前版本(截至 SDK 1.2.0)的标准库还没有内置 Result,直接写 Result<...> 会报 undeclared type name 'Result',当时我们用一个普通枚举手写了"结果"类型。有了泛型,可以把它写得更通用——成功装一种数据,失败装另一种,这也是将来官方 Result<T, E> 的形态:
enum MyResult<T, E> {
| Ok(T)
| Err(E)
}
func divide(a: Int64, b: Int64): MyResult<Int64, String> {
if (b == 0) {
return Err("除数不能为 0")
}
return Ok(a / b)
}
main(): Int64 {
match (divide(10, 3)) {
case Ok(v) => println("成功:${v}") // 成功:3
case Err(msg) => println("失败:${msg}")
}
match (divide(10, 0)) {
case Ok(v) => println("成功:${v}")
case Err(msg) => println("失败:${msg}") // 失败:除数不能为 0
}
return 0
}
看懂这两段,你就彻底理解了第 9、10 课:"可空"和"错误"都不是什么特殊语法——Option 是标准库提供的枚举,而 Result 形态的结果类型,我们自己用一个枚举也能写出来,配合 match 强制调用方处理"没有值/出错了"的分支。等将来 SDK 内置 Result,把 MyResult/Ok/Err 换个名字即可完成迁移(第 10 课已预告过这一点)。
6.3 枚举也能实现接口
和 class 一样,枚举用 <: 实现接口,照样能多态:
interface Drawable {
func draw(): Unit
}
enum TrafficLight <: Drawable {
| Red | Yellow | Green
public func draw(): Unit {
match (this) {
case Red => println("画红灯")
case Yellow => println("画黄灯")
case Green => println("画绿灯")
}
}
}
let lights: Array<Drawable> = [Red, Yellow, Green] 后统一调 l.draw(),走的就是第 17 课的接口多态。
6.4 想打印、想 == 比较:实现 ToString / Equatable
枚举默认不能直接 println,也不能用 == 比较,编译器会分别报 mismatched types 和 invalid binary operator '=='。需要这两个能力时,实现对应接口:
enum Coin <: Equatable<Coin> & ToString {
| Penny | Nickel | Dime | Quarter
// Equatable 要求:实现 ==(!= 会自动获得)
public operator func ==(other: Coin): Bool {
match ((this, other)) { // 元组模式:同时匹配两个值
case (Penny, Penny) => true
case (Nickel, Nickel) => true
case (Dime, Dime) => true
case (Quarter, Quarter) => true
case _ => false
}
}
// ToString 要求:实现 toString 后即可 println / 字符串插值
public func toString(): String {
match (this) {
case Penny => "1美分"
case Nickel => "5美分"
case Dime => "10美分"
case Quarter => "25美分"
}
}
}
实测:Penny == Penny 为 true、Penny != Dime 为 true(两者确实不同,!= 由接口自动提供)、println(Nickel) 输出 5美分。
6.5 Comparable 与 Ordering:让枚举能排序
实现 Comparable<T> 需要写 compare 方法,它的返回值是标准库枚举 Ordering,只有三个构造器:LT(小于)、EQ(等于)、GT(大于)。实现后自动获得 <、>、== 全套比较:
enum Priority <: Comparable<Priority> {
| Low | Medium | High
func rank(): Int64 {
match (this) {
case Low => 1
case Medium => 2
case High => 3
}
}
public func compare(other: Priority): Ordering {
let d = this.rank() - other.rank()
if (d < 0) {
return Ordering.LT
} else if (d > 0) {
return Ordering.GT
} else {
return Ordering.EQ
}
}
}
排序沿用第 7、12 课的全局函数 sort(数组, by: { ... })(import std.sort.*),比较器直接委托给 compare:
sort(tasks, by: { a: Task, b: Task => b.priority.compare(a.priority) })
七、实战:待办事项状态机
把本课知识点串起来:一个待办清单,任务有优先级(可排序的枚举)和状态(关联值携带进度、完成时间的枚举),所有状态流转都靠 match 保证合法。
package todo
import std.sort.*
// 优先级:实现 Comparable,自带 compare 产出 Ordering
enum Priority <: Comparable<Priority> {
| Low | Medium | High
func rank(): Int64 {
match (this) {
case Low => 1
case Medium => 2
case High => 3
}
}
public func compare(other: Priority): Ordering {
let d = this.rank() - other.rank()
if (d < 0) {
return Ordering.LT
} else if (d > 0) {
return Ordering.GT
} else {
return Ordering.EQ
}
}
func text(): String {
match (this) {
case Low => "低"
case Medium => "中"
case High => "高"
}
}
}
// 任务状态:关联值携带进度和完成时间
enum Status {
| Pending // 未开始
| Doing(UInt8) // 进行中,0~100
| Done(Int64) // 已完成,携带完成时间戳
func text(): String {
match (this) {
case Pending => "未开始"
case Doing(p) => "进行中 ${p}%"
case Done(ts) => "已完成(时间戳=${ts})"
}
}
}
class Task {
public let id: Int64
public let title: String
public let priority: Priority
public var status: Status
public init(id: Int64, title: String, priority: Priority) {
this.id = id
this.title = title
this.priority = priority
this.status = Pending
}
}
// 开始任务:只有"未开始"能开始
func start(t: Task): Bool {
match (t.status) {
case Pending =>
t.status = Doing(0)
return true
case _ =>
return false
}
}
// 更新进度:只有进行中能更新,且取值合法
func updateProgress(t: Task, p: UInt8): Bool {
match (t.status) {
case Doing(_) where p <= 100 =>
t.status = Doing(p)
return true
case _ =>
return false
}
}
// 完成任务:已完成的不能重复完成,其他状态都可完成
func finish(t: Task, ts: Int64): Bool {
match (t.status) {
case Done(_) => false
case _ =>
t.status = Done(ts)
return true
}
}
// 紧急任务:高优先级且尚未完成
func isUrgent(t: Task): Bool {
match (t.status) {
case Done(_) => false
case _ => t.priority == High
}
}
func oneLine(t: Task): String {
return "#${t.id} [${t.priority.text()}] ${t.title} —— ${t.status.text()}"
}
main(): Int64 {
let tasks = [
Task(1, "写第18课教程", High),
Task(2, "回复评论区", Low),
Task(3, "修复 LSP 崩溃", High),
Task(4, "整理发布说明", Medium)
]
// 模拟操作
start(tasks[0])
updateProgress(tasks[0], 60)
start(tasks[2])
finish(tasks[2], 1727000000)
finish(tasks[3], 1727000100)
println("===== 全部任务 =====")
for (t in tasks) {
println(oneLine(t))
}
println("\n===== 紧急任务(高优先级且未完成)=====")
for (t in tasks) {
if (isUrgent(t)) {
println(oneLine(t))
}
}
// 按优先级从高到低排序(比较器委托给 Priority.compare)
sort(tasks, by: { a: Task, b: Task => b.priority.compare(a.priority) })
println("\n===== 按优先级从高到低 =====")
for (t in tasks) {
println(oneLine(t))
}
return 0
}
运行结果:
===== 全部任务 =====
#1 [高] 写第18课教程 —— 进行中 60%
#2 [低] 回复评论区 —— 未开始
#3 [高] 修复 LSP 崩溃 —— 已完成(时间戳=1727000000)
#4 [中] 整理发布说明 —— 已完成(时间戳=1727000100)
===== 紧急任务(高优先级且未完成)=====
#1 [高] 写第18课教程 —— 进行中 60%
===== 按优先级从高到低 =====
#1 [高] 写第18课教程 —— 进行中 60%
#3 [高] 修复 LSP 崩溃 —— 已完成(时间戳=1727000000)
#4 [中] 整理发布说明 —— 已完成(时间戳=1727000100)
#2 [低] 回复评论区 —— 未开始
这个例子值得体会的设计:每个操作函数返回 Bool 表示"这次状态流转是否合法",非法流转在编译期就被 match 的分支结构框死了——比如对 Done 的任务再 finish,不用任何 if 判空,天然返回 false。这就是用类型表达业务规则的威力。
八、常见问题 FAQ
Q1:仓颉枚举为什么没有 Mon = 1 这种原始值?
仓颉枚举是代数数据类型,不是其他语言里"整数的别名",构造器不自动对应整数(写 Mon = 1 会直接语法报错)。想要数字编号,自己写一个 match 方法转换(如本课的 rank());想让构造器携带数据,用有参构造器 Blue(UInt8)。这反而比原始值强大得多——关联值可以是任意类型、任意个数。
Q2:枚举为什么不能直接 println、不能用 ==?
枚举默认不实现任何接口。打印需要 <: ToString 并写 toString();比较需要 <: Equatable<T> 并写 operator func ==(实现后 != 自动可用);想排序就实现 Comparable<T> 的 compare。日常最简单的判断方式其实是 match,多数场景根本不需要 ==。
Q3:枚举能写在函数里面吗?
不能,枚举只能定义在源文件顶层。函数内部需要"临时的多选一"时,直接用顶层枚举或 match 字面分支即可。
Q4:case 的书写顺序重要吗?
重要。match 自上而下匹配,第一个命中的分支胜出后立刻跳出。所以带 where 守卫的"小范围"分支必须写在"大范围"分支前面(Circle(r) where r > 10 要在 Circle(_) 之前),否则永远轮不到它。
Q5:报 non-exhaustive patterns 怎么办?
说明有构造器没覆盖,报错信息的 note 会直接告诉你漏了谁(如 Winter is not covered)。要么补全分支,要么在最后加 case _ => 兜底。不要无脑加 _——穷尽检查是编译器送的安全网,显式列全每个构造器通常更安全。
Q6:match 和 if-let 什么时候用哪个?
需要覆盖所有/多种构造器、各分支行为不同时用 match;只关心某一种构造器、其他情况统一处理时用 if-let。处理 Option(第 9 课)或手写的结果枚举(第 10 课)时这两者最常用。
Q7:枚举最后的 ... 是什么?
那是非穷举枚举声明,表示"未来可能追加新构造器":
enum ExtendedColor {
| Red | Green | Blue
| ...
}
对它做 match 时必须写 case _ 兜底(编译器不认为三个构造器就是全部),这样将来新增构造器时旧代码仍能编译。自己的业务枚举形态固定时不需要它。
Q8:枚举和第 17 课的 sealed 类怎么选?
形态封闭、只装数据、几乎没有各自的行为时用枚举(订单状态、表达式树、Option/Result);各形态需要独立的字段、复杂方法或被外部继承时用 sealed 类。经验法则:先问"这是不是一组数据形态"——是,就用枚举。
九、课后练习
- 定义枚举
enum Weekday { | Mon | Tue | Wed | Thu | Fri | Sat | Sun },写函数isWeekend(d: Weekday): Bool(用多模式合并),再写一个text(d)返回中文星期。 - 给本课的
Shape枚举增加Triangle(Float64, Float64, Float64)(三边,用海伦公式√(p(p-a)(p-b)(p-c)),p=(a+b+c)/2求面积),在area和describe中补全分支;故意删掉一个分支,观察non-exhaustive patterns报错。 - 给第五节的
Expr增加Div(Expr, Expr)除法构造器:除数求值为 0 时让eval返回Option<Int64>(None表示除零错误),调用方用 match 处理两种结果。 - 定义
enum HttpStatus { | Ok(Int64) | Redirect(Int64, String) | Error(Int64, String) },实现ToString,用嵌套绑定/多模式让200/204都显示"成功"。 - 扩展实战项目:给
Status增加Paused(UInt8)(暂停时保留进度),新增pause(t)(只有 Doing 能暂停)和resume(t)(只有 Paused 能恢复为 Doing)。编译后按编译器的not covered提示,把所有需要补分支的函数逐个补全——亲手感受一遍穷尽检查的价值。
下节预告
本课的 MyOption<T>、MyResult<T, E> 已经提前用了泛型:类型本身留一个"占位符 T",使用时再决定装什么类型。第 19 课 泛型编程将系统讲解泛型函数、泛型类/结构体、泛型约束(where T <: Comparable<T>),让你写出"一份逻辑,任意类型复用"的容器与算法——第 8 课的 ArrayList<T>、第 14 课的 Iterator<T> 背后都是它。
系列说明:本系列基于 Windows 平台 + CIDE + 仓颉 SDK(1.2.0)编写,所有代码均已实际编译运行通过。如遇 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)