【仓颉语言入门 · 第14课】
第 13 课用闭包做出了"能记住状态的函数",这节课解决一个更实际的问题:想逐个产出一系列值——大文件的逐行读取、传感器源源不断的读数、甚至斐波那契那样无穷无尽的数列——难道都要先一口气算完塞进数组?当然不是。仓颉的答案是迭代器(Iterator):用一个
next()方法按需取值,用多少算多少。本文所有代码与报错文案均在仓颉 SDK 1.1.3 下逐行实测编译运行。途中会澄清四个高频误区(每一个都是编译器亲自告诉我的):① 仓颉 1.1.3 没有 Python/JS 那种
yield/sequence { }生成器语法糖,惰性序列靠手写迭代器类;② 数组上直接调map/filter是立即求值返回新数组,先iterator()再调才是惰性链;③Iterator和Iterable是两个并列接口,不是继承关系;④ 迭代器上没有toArray()、drop()、chain()、sum(),正确的名字分别是"手动收集"、skip()、concat()、fold()。
目录(系列导航)
整套路线共 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 数据处理程序
一、先想清楚:为什么要"懒"
前 8 课处理数据,套路都是"全部装进容器,再遍历处理":
let nums = [1, 2, 3, 4, 5]
for (n in nums) {
println(n)
}
这在数据量小、数量有限时毫无问题。但设想三种场景:
- 数据量巨大:一个 10 GB 的日志文件,你只想找出前 10 条错误。一次性读进内存?机器直接卡死。
- 数据无限:斐波那契数列没有尽头,自然数 1、2、3……永远数不完,根本没法"先全部装进数组"。
- 数据还没产生:传感器每秒上报一次读数,处理程序必须"来一个处理一个"。
这三种场景的共同点是:值不应该提前算好,而应该在被需要的那一刻才算出来。这就是"惰性求值(lazy evaluation)"。迭代器就是承载惰性求值的对象——它像一个"游标",你每叫一次 next(),它才往前走一步、吐出一个值。
二、Iterator 接口:一个 next() 方法的协议
仓颉标准库中,迭代器的定义极其简单(std.core 包,无需 import):
public interface Iterator<T> {
func next(): Option<T>
}
就一个方法:
- 返回
Some(值):游标当前位置有值,同时游标前进一步; - 返回
None:序列到头了,以后再调也永远是None。
自己写一个迭代器,只要实现这个接口。注意仓颉的语法:接口实现用 <:,接口方法在实现类里要加 public:
class Counter <: Iterator<Int64> {
var current: Int64 = 1
let limit: Int64
init(limit: Int64) {
this.limit = limit
}
public func next(): Option<Int64> {
if (current <= limit) {
let v = current
current++
return Some(v)
}
return None
}
}
🚫 仓颉的类没有主构造函数简写。写
class Counter(var current: Int64, let limit: Int64)(Kotlin 风格)会直接报expected '{', found '('。必须先在类体里声明字段,再写init构造函数。第 15 课会专门讲,这里先照抄格式。
三、消费迭代器的三种姿势
3.1 手动调 next():while + match
迭代器最原始的消费方式。仓颉 1.1.3 没有 while let Some(v) = ... 这类语法糖,标准写法是 while (true) 配合第 4、9 课的 match:
main(): Int64 {
let c = Counter(3)
while (true) {
match (c.next()) {
case Some(v) => print("${v} ")
case None => break
}
}
println()
return 0
}
输出:
1 2 3
None 一出现就 break,游标到此耗尽。
3.2 for-in:最常用
实现了 Iterator 的对象可以直接放进 for-in,编译器帮你重复调 next() 并在 None 时停止:
for (v in Counter(5)) {
print("${v} ")
}
1 2 3 4 5
3.3 终端方法:函数式一把梭
迭代器上挂了十几个现成方法(第六节细讲),forEach 是其中最直接的——对每个值执行一个 Lambda:
Counter(3).forEach { v => print("${v} ") }
四、Iterable 接口:让自定义容器支持 for-in
Counter 那种"自己就是游标"的对象适合一次性遍历。但更多时候我们有的是容器(书架、成绩表、传感器一周的数据)——容器希望能被反复遍历,每次遍历都从头开始。
这时用另一个接口 Iterable<T>,它同样只有一个方法:
public interface Iterable<T> {
func iterator(): Iterator<T>
}
注意它的方法签名:返回一个迭代器。这是一个"工厂方法"——每调用一次,就生产出一个游标在起点的全新迭代器。
完整套路是"容器 + 专属游标类"两件套:
// 游标:持有容器引用和当前下标
class RangeIter <: Iterator<Int64> {
var current: Int64
let end: Int64
init(start: Int64, end: Int64) {
this.current = start
this.end = end
}
public func next(): Option<Int64> {
if (current <= end) {
let v = current
current++
return Some(v)
}
return None
}
}
// 容器:每次被遍历都 new 一个新游标
class IntRange <: Iterable<Int64> {
let start: Int64
let end: Int64
init(start: Int64, end: Int64) {
this.start = start
this.end = end
}
public func iterator(): Iterator<Int64> {
return RangeIter(start, end)
}
}
main(): Int64 {
for (v in IntRange(1, 5)) {
print("${v} ")
}
println()
return 0
}
1 2 3 4 5
标准库的 Array、ArrayList、Range、HashMap(遍历键值对)走的都是这套协议。第 6 课学的字符串字节遍历和 runes() 字符遍历,本质也是迭代器。
🚫 如果一个类光写了
iterator()方法却不声明<: Iterable<T>,for-in会报错:the type ... does not implement Iterator。仓颉不看"方法长得像不像",只认接口声明。
五、Iterator 与 Iterable:一次性游标 vs 可重复遍历
这是本节课最容易混淆、也最关键的一点。两个接口是并列关系,不是继承(我试过把 Iterable 对象赋给 Iterator 变量,编译器直接 mismatched types)。分工如下:
Iterator<T> | Iterable<T> | |
|---|---|---|
| 角色 | 游标(有状态、会前进) | 容器(数据本身) |
| 要实现的方法 | next(): Option<T> | iterator(): Iterator<T> |
| 能否重复遍历 | 不能,耗尽即空 | 能,每次产生新游标 |
| 典型代表 | arr.iterator() 的返回值 | 数组、ArrayList、Range |
实测"迭代器一次性":
main(): Int64 {
let arr = [10, 20, 30]
let it = arr.iterator()
let n1 = it.fold(0) { acc, _ => acc + 1 }
let n2 = it.fold(0) { acc, _ => acc + 1 }
println("第一次:${n1},第二次:${n2}")
// 直接遍历数组(Iterable),多少次都行
let n3 = arr.iterator().fold(0) { acc, _ => acc + 1 }
println("重新拿一个游标:${n3}")
return 0
}
第一次:3,第二次:0
重新拿一个游标:3
同一个 it 遍历一遍后游标已到尽头,第二遍什么都没有(0 个);而向数组(Iterable)再要一个 iterator(),又是一条好汉。
📌 经验法则:自己写数据容器,实现
Iterable;只想生成一串值(尤其是算出来的、无限的),实现Iterator。
六、惰性流水线:map / filter / take / skip
迭代器之所以好用,是因为 Iterator 上挂了一串链式方法,写法和第 12 课的数组 Lambda 几乎一样,但语义完全不同:
6.1 先看数组上的链:立即求值
let nums = [1, 2, 3, 4, 5, 6, 7, 8]
let r = nums.map { x => x * 2 }.filter { x => x > 10 }.take(2)
println("${r}")
[12, 14]
这条链每一步都立刻执行并产出一个新数组:map 先造一个 8 元素数组,filter 再造一个,take 再造一个。数据量大时中间数组会浪费大量内存。
6.2 再看迭代器上的链:惰性求值
只要在开头加一个 .iterator(),整条链的性质就变了:
let r2 = nums.iterator()
.map { x => x * 2 }
.filter { x => x > 10 }
.take(2)
.forEach { v => print("${v} ") }
map/filter/take/skip称为中间操作:它们不立刻计算,只是返回一个"记住了规则"的新迭代器,像一串扣在一起的齿轮;forEach/fold/any等称为终端操作:只有调用它们,齿轮才真正转动,开始一个一个地next()拉取;take(2)拉够 2 个就停——上游不会多算一个值。
空口无凭,看实测。在 map 里塞一行打印当"探针",只取 3 个值:
main(): Int64 {
let data = [10, 20, 30, 40, 50]
let lazyMapped = data.iterator().map { x =>
println(" ...正在计算 ${x}")
x * 2
}
println("只取前 3 个:")
lazyMapped.take(3).forEach { v => println("得到 ${v}") }
return 0
}
只取前 3 个:
...正在计算 10
得到 20
...正在计算 20
得到 40
...正在计算 30
得到 60
5 个数据,只算了前 3 个——40 和 50 的 map 规则一次都没执行。这就是惰性:终端操作要多少,中间操作才算多少。如果这是 10 GB 日志,省下的就是巨量内存和时间。
6.3 常用中间操作
| 方法 | 作用 |
|---|---|
map { x => ... } | 逐个变换值的类型/内容 |
filter { x => 条件 } | 只保留满足条件的值 |
take(n) | 只取前 n 个,然后结束 |
skip(n) | 跳过前 n 个,从第 n+1 个开始 |
flatMap { x => 迭代器 } | 一对多展开,再拍平成一条流 |
zip(另一个迭代器) | 两条流配对,产出 (a, b) 元组,短的到头就结束 |
concat(另一个迭代器) | 先流完自己,再接上流另一个 |
enumerate() | 给每个值贴上从 0 开始的编号,产出 (下标, 值) 元组 |
flatMap 示例(每个值展开成两个):
[1, 2, 3].iterator()
.flatMap { x => [x, x * 10].iterator() }
.forEach { v => print("${v} ") }
1 10 2 20 3 30
⚠️ 元组不能直接字符串插值。
zip/enumerate产出的元组写"${p}"会报Tuple ... should implement interface 'ToString',要用下标取分量:p[0]、p[1]。
七、无限序列:有了 take,无穷也敢遍历
惰性迭代器最震撼的用法:序列可以是无限的。因为反正你不主动拉,它一个值都不会算;再配合 take 设好"安全阀",无限流就变成了"想要多少取多少"。
实测一个无限的斐波那契数列生成器:
class Fib <: Iterator<Int64> {
var a: Int64 = 0
var b: Int64 = 1
public func next(): Option<Int64> {
let v = a
a = b
b = v + b
return Some(v) // 永远返回 Some,序列没有尽头
}
}
main(): Int64 {
print("前 10 个斐波那契数:")
Fib().take(10).forEach { x => print("${x} ") }
println()
return 0
}
前 10 个斐波那契数:0 1 1 2 3 5 8 13 21 34
Fib() 本身是无限的,直接 for (v in Fib()) 会死循环;但 take(10) 拉满 10 个就发出结束信号,程序安然无恙。自然数、素数流、随机数流都是同一个套路。
🚫 仓颉 1.1.3 没有生成器语法糖。 网上看到仓颉(或其他语言)教程里写
sequence<Int64> { yield(1); yield(2) }、或在普通函数里写yield(x)当生成器——本版本编译器会报undeclared identifier 'sequence'/undeclared identifier 'yield'。仓颉的做法就是老老实实写一个实现Iterator的类,把"状态"放进字段(比如 Fib 的a、b),在next()里推进状态。啰嗦一点,但逻辑完全透明。
八、终端操作与 API 速查
终端操作一调用,整条惰性链开始真正运算,并给出一个最终结果(而不是另一个迭代器)。均在 1.1.3 实测:
| 方法 | 返回 | 说明 |
|---|---|---|
forEach { x => ... } | Unit | 逐值执行副作用(打印、写入文件等) |
fold(初值) { acc, x => ... } | 累加结果 | 最通用的聚合,求和/计数/拼接都靠它 |
reduce { a, b => ... } | Option<R> | 无初值聚合,空流返回 None,更安全 |
count() | Int64 | 统计剩余元素个数(不接受条件,要条件计数先 filter) |
any { x => 条件 } | Bool | 是否存在满足条件的值(找到即停) |
all { x => 条件 } | Bool | 是否全部满足(遇到反例即停) |
first() | Option<T> | 第一个值,空流为 None |
min() / max() | Option<T> | 最小/最大值,空流为 None |
几个实测要点:
① 条件计数用 filter(...).count(),因为 count() 不带参数:
let n = [1, 2, 3, 4].iterator().filter { x => x > 2 }.count()
println("${n}") // 2
② reduce / min / max / first 返回 Option,空流不抛异常,用 match 或 ?? 兜底:
match (ArrayList<Int64>().iterator().min()) {
case Some(v) => println("最小值 ${v}")
case None => println("空的,没有最小值")
}
空的,没有最小值
③ 迭代器上没有 toArray() / toArrayList(),想把惰性链的结果收成集合,手动接进 ArrayList(forEach 里调用的是容器方法,不捕获外层 var,合法):
let list = ArrayList<Int64>()
Fib().take(5).forEach { x => list.add(x) }
let arr = list.toArray()
println("${arr}") // [0, 1, 1, 2, 3]
④ 一张"别写错名字"对照表(左列在 1.1.3 迭代器上不存在,右列才是对的):
| 别写 | 正确写法 |
|---|---|
iter.drop(n) | iter.skip(n) |
iter1.chain(iter2) | iter1.concat(iter2) |
iter.sum() | iter.fold(0) { a, b => a + b } |
iter.toArray() | forEach 手动收集进 ArrayList,再 toArray() |
iter.count { x => ... } | iter.filter { x => ... }.count() |
九、CIDE 实操:气温监测数据流
9.1 编写程序
综合运用本节课全部要素:自定义无限迭代器 + 自定义 Iterable 容器、惰性 filter/map/take、fold/reduce 聚合、any/all 判定、min/max 安全取值、enumerate 编号、concat 拼接。
场景:气象站一周气温读数,用 -99 标记传感器缺失的数据。需要过滤缺失值并完成统计;另用一个无限自然数流演示"无限数据 + take 安全阀"。
package WeatherIter
import std.collection.*
// ① 无限迭代器:从 1 开始的自然数流
class Naturals <: Iterator<Int64> {
var n: Int64 = 1
public func next(): Option<Int64> {
let v = n
n++
return Some(v)
}
}
// ② 容器:一周气温读数
class WeeklyTemps <: Iterable<Int64> {
let data: Array<Int64>
init(data: Array<Int64>) { this.data = data }
public func iterator(): Iterator<Int64> {
return TempIter(data)
}
}
// 容器专属游标
class TempIter <: Iterator<Int64> {
let data: Array<Int64>
var i: Int64 = 0
init(data: Array<Int64>) { this.data = data }
public func next(): Option<Int64> {
if (i < data.size) {
let v = data[i]
i++
return Some(v)
}
return None
}
}
main(): Int64 {
// —— 无限流也敢用,take 兜底 ——
print("前 10 个平方数:")
Naturals().map { x => x * x }.take(10).forEach { v => print("${v} ") }
println()
// —— 真实容器的惰性流水线 ——
let week = WeeklyTemps([21, -99, 23, -99, 25, 22, 30])
// 个数与总和(count 不接受条件,先 filter)
let count = week.iterator().filter { t => t != -99 }.count()
let total = week.iterator().filter { t => t != -99 }.fold(0) { a, b => a + b }
println("有效天数:${count},平均气温:${total / count}℃")
// reduce 无初值求和:空流天然得到 None
let valid = week.iterator().filter { t => t != -99 }
match (valid.reduce { a, b => a + b }) {
case Some(s) => println("reduce 总和:${s}")
case None => println("没有有效读数")
}
// 判定类终端操作
println("是否全部高于 20℃:${week.iterator().filter { t => t != -99 }.all { t => t > 20 }}")
println("是否出现过 30℃:${week.iterator().any { t => t == 30 }}")
// 最高温:max() 返回 Option,用 ?? 兜底
println("最高温:${week.iterator().filter { t => t != -99 }.max() ?? -1}℃")
// 带编号展示前 3 个有效读数(元组用 p[0]/p[1] 取值)
print("前 3 个有效读数:")
week.iterator().filter { t => t != -99 }.take(3).enumerate().forEach { p =>
print("第${p[0] + 1}天=${p[1]}℃ ")
}
println()
// concat 拼接两周数据
let week2 = WeeklyTemps([19, 20])
print("两周连读前 4 个:")
week.iterator().concat(week2.iterator()).take(4).forEach { v => print("${v} ") }
println()
return 0
}
9.2 运行与验证
前 10 个平方数:1 4 9 16 25 36 49 64 81 100
有效天数:5,平均气温:24℃
reduce 总和:121
是否全部高于 20℃:true
是否出现过 30℃:true
最高温:30℃
前 3 个有效读数:第1天=21℃ 第2天=23℃ 第3天=25℃
两周连读前 4 个:21 -99 23 -99
关键点解读:
Naturals永远返回Some,是真正的无限流;take(10)像闸门,拉够 10 个就关,map不会被执行第 11 次。WeeklyTemps是容器(Iterable),持有原始数组;每次iterator()都 new 一个TempIter,所以下面能对同一周数据反复做 5 次统计,互不干扰。-99的缺失值统一在filter阶段剔除,后续所有统计拿到的都是干净数据——这正是流水线的价值:过滤逻辑只写一次。max()返回Option<Int64>,?? -1在全空时兜底,绝不会抛异常。enumerate()产出元组,必须p[0]、p[1]取分量。
可以自己动手改:
- 把
take(10)改成take(20),确认平方数继续正确产出、且程序正常退出; - 把周数据改成全
-99,观察reduce走None分支、max()打印兜底值-1; - 用
fold计算有效气温的平均值(提示:先求和、再除以count,注意整数除法)。
9.3 用调试器观察惰性求值
- 在
Naturals的next()里let v = n一行打断点,Shift + F9 启动调试; - 单步观察:
take(10)驱动下,next()恰好被调用 10 次,第 11 次永远不会发生——亲眼确认"终端操作要多少才算多少"; - 在
TempIter.next()打断点,观察filter剔除-99时游标如何"多走一步"才吐出一个有效值; - 展开 Variables 面板里的
this,对比每次iterator()产生的TempIter的i都是独立从 0 开始的。
调试结束点停止按钮。
十、常见问题 FAQ
Q1:迭代器和数组遍历,日常写代码该用哪个? 数据已经在数组里、且要多次使用,直接遍历数组(最简单)。迭代器的价值在三种情况:数据量很大/无限、数据是边产生边处理的、想把多个变换串成一条不产生中间集合的流水线。
Q2:Iterator 和 Iterable 到底怎么选?
你在描述"一个数据集合"(能被反复遍历)→ 实现 Iterable,配一个游标类;你在描述"一串按规则生成的值"(尤其是算出来的、无限的、一次性的)→ 直接实现 Iterator。
Q3:为什么我的类写了 iterator() 方法,for-in 还是报错?
光有方法不够,必须在类声明里写 <: Iterable<T>。仓颉的 for-in 只认接口,不认"结构上恰好长这样"的鸭子类型。报错文案是 does not implement Iterator。
Q4:仓颉有 yield / 生成器函数吗?
1.1.3 没有。sequence { } 块和函数体里的 yield(x) 都会报"未声明标识符"。生成序列的唯一方式是手写实现 Iterator 的类,用字段保存状态、在 next() 里推进。
Q5:为什么 arr.map { ... } 和 arr.iterator().map { ... } 写法差不多,要强调区别?
前者每一步立即产出新数组(中间结果全部驻留内存);后者只在终端操作时按需计算,不产生中间数组。数据量小看不出差别,处理大文件/无限流时是天壤之别。
Q6:迭代器遍历一遍后还能再遍历吗?
同一个迭代器对象不能,游标耗尽后再调 next() 永远是 None。但 Iterable 容器每次 iterator() 都会给你一个新游标,可以反复遍历。需要"再来一遍"时,重新调用 iterator()。
Q7:zip / enumerate 的结果为什么不能直接打印?
它们产出的是元组,标准库的元组没有实现 ToString,字符串插值会报错。用下标取分量:p[0]、p[1],或者解构后再打印。
Q8:怎么把迭代器结果存回数组?
迭代器上没有 toArray()。先建一个 ArrayList<T>(),用 forEach { x => list.add(x) } 收集,再调 list.toArray()。
Q9:for-in 无限迭代器会怎样?
会死循环(无限迭代器的 next() 永远返回 Some)。无限流必须用 take(n)、any { 条件 } 这类自带终止能力的操作消费,确保它迟早会停。
Q10:reduce 和 fold 有什么区别?空流会崩吗?
fold(初值) { ... } 必须给初值,空流直接返回初值;reduce { ... } 不给初值,返回 Option,空流得到 None。二者都不会因为空序列崩溃——这比很多语言抛异常的设计更安全。
十一、课后练习
- 写一个
Countdown <: Iterator<Int64>,从构造参数 n 开始倒着数数到 1,之后返回None。用 for-in 打印Countdown(5),应输出5 4 3 2 1。 - 给第 1 题的倒计时接上链式操作:只保留偶数、每个数平方、用
forEach打印。预期输出16 4(即 4²、2²)。 - 写一个无限迭代器
MultiplesOf(k: Int64),依次产出 k 的所有倍数(k、2k、3k……)。用take(5)安全地取出 3 的前 5 个倍数并打印。 - 用
fold实现一个函数,接收一个Iterator<Int64>,返回其中正数的平均值(提示:可以 fold 出一个"个数+总和"的二元组,元组字段用[0]、[1]访问;注意空流除零,用 match 处理)。 - 综合:定义一个
Sentence容器实现Iterable<String>(内部用一个Array<String>存单词),写专属游标类。然后用迭代器链完成:过滤掉长度小于 3 的单词、enumerate编号、只取前 3 个、按#编号:单词的格式打印。
下节预告
函数式模块(11~14 课)到此收官:从命名函数、匿名 Lambda、闭包捕获,到今天按需取值的迭代器,你已经能用"值 + 函数"表达相当复杂的逻辑了。但写大型程序还需要另一套组织代码的武器——把数据和操作数据的方法绑在一起。第 15 课 结构体 struct 与类 class 正式进入面向对象世界:值类型与引用类型的根本差异、什么时候该用谁、init 构造函数的完整规则。我们下节课见!
系列说明:本系列基于 Windows 平台 + CIDE + 仓颉 SDK(1.1.3)编写,所有代码均已实际编译运行通过。如遇 SDK 版本差异导致的细节出入(例如新版本补充了
yield生成器语法、为迭代器增加了toArray()等),以你本地版本为准,欢迎评论区交流。
💬 遇到问题?扫码联系作者
跟着课程练习时,如果在 SDK 安装、环境变量配置、编译报错或调试上卡住,欢迎扫码加作者企业微信直接咨询(请备注"仓颉课程"):
离线环境下图片可能加载不出来,也可以在 CIDE 菜单 Help ▸ 联系作者 / Contact 中查看同一张二维码(应用内置兜底图,无需联网)。
📥 工具下载
本系列全程使用的仓颉 IDE —— CIDE(免费开源、社区版):
- GitCode 仓库 / 安装包下载:gitcode.com/wp_upala/ci…
- 打开页面后进入 发行版(Releases),两种包任选其一:
- 安装版:下载
CIDE-<版本>-x64-Setup.exe,双击安装,适合日常长期使用; - 免安装版(Portable):下载
CIDE-<版本>-x64-Portable.zip,解压到任意目录即用,不写注册表、不留安装痕迹,拷到 U 盘也能在别的电脑直接运行(包内附《使用说明.txt》)。适合先试用、或在受限电脑上学习本系列课程。
- 安装版:下载
- 仓颉 SDK 请前往仓颉编程语言官网下载:cangjie-lang.cn