【仓颉语言入门 · 第14课】迭代器 Iterator 与惰性序列
第 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 数据处理实战 |
一、先想清楚:为什么要"懒"
前 8 课处理数据,套路都是"全部装进容器,再遍历处理":
let nums = [1, 2, 3, 4, 5]
for (n in nums) {
println(n)
}
这在数据量小、数量有限时毫无问题。但设想三种场景:
这三种场景的共同点是:值不应该提前算好,而应该在被需要的那一刻才算出来。这就是"惰性求值(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)。分工如下:
| 角色 | 游标(有状态、会前进) | 容器(数据本身) |
| 要实现的方法 | 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 用调试器观察惰性求值
调试结束点停止按钮。
十、常见问题 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。二者都不会因为空序列崩溃——这比很多语言抛异常的设计更安全。
十一、课后练习
下节预告
函数式模块(11~14 课)到此收官:从命名函数、匿名 Lambda、闭包捕获,到今天按需取值的迭代器,你已经能用"值 + 函数"表达相当复杂的逻辑了。但写大型程序还需要另一套组织代码的武器——把数据和操作数据的方法绑在一起。第 15 课 结构体 struct 与类 class 正式进入面向对象世界:值类型与引用类型的根本差异、什么时候该用谁、init 构造函数的完整规则。我们下节课见!
系列说明:本系列基于 Windows 平台 + CIDE + 仓颉 SDK(1.1.3)编写,所有代码均已实际编译运行通过。如遇 SDK 版本差异导致的细节出入(例如新版本补充了 yield 生成器语法、为迭代器增加了 toArray() 等),以你本地版本为准,欢迎评论区交流。
💬 遇到问题?扫码联系作者
跟着课程练习时,如果在 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
网硕互联帮助中心







评论前必须登录!
注册