【仓颉语言入门 · 第14课】

0 阅读15分钟

【仓颉语言入门 · 第14课】

第 13 课用闭包做出了"能记住状态的函数",这节课解决一个更实际的问题:想逐个产出一系列值——大文件的逐行读取、传感器源源不断的读数、甚至斐波那契那样无穷无尽的数列——难道都要先一口气算完塞进数组?当然不是。仓颉的答案是迭代器(Iterator):用一个 next() 方法按需取值,用多少算多少。

本文所有代码与报错文案均在仓颉 SDK 1.1.3 下逐行实测编译运行。途中会澄清四个高频误区(每一个都是编译器亲自告诉我的):① 仓颉 1.1.3 没有 Python/JS 那种 yield / sequence { } 生成器语法糖,惰性序列靠手写迭代器类;② 数组上直接调 map / filter立即求值返回新数组,先 iterator() 再调才是惰性链;③ IteratorIterable 是两个并列接口,不是继承关系;④ 迭代器上没有 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 数据处理实战 |

  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 数据处理程序

一、先想清楚:为什么要"懒"

前 8 课处理数据,套路都是"全部装进容器,再遍历处理":

let nums = [1, 2, 3, 4, 5]
for (n in nums) {
    println(n)
}

这在数据量小、数量有限时毫无问题。但设想三种场景:

  1. 数据量巨大:一个 10 GB 的日志文件,你只想找出前 10 条错误。一次性读进内存?机器直接卡死。
  2. 数据无限:斐波那契数列没有尽头,自然数 1、2、3……永远数不完,根本没法"先全部装进数组"。
  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

标准库的 ArrayArrayListRangeHashMap(遍历键值对)走的都是这套协议。第 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 个——4050map 规则一次都没执行。这就是惰性:终端操作要多少,中间操作才算多少。如果这是 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 的 ab),在 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(),想把惰性链的结果收成集合,手动接进 ArrayListforEach 里调用的是容器方法,不捕获外层 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/takefold/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,观察 reduceNone 分支、max() 打印兜底值 -1
  • fold 计算有效气温的平均值(提示:先求和、再除以 count,注意整数除法)。

9.3 用调试器观察惰性求值

  1. Naturalsnext()let v = n 一行打断点,Shift + F9 启动调试;
  2. 单步观察:take(10) 驱动下,next() 恰好被调用 10 次,第 11 次永远不会发生——亲眼确认"终端操作要多少才算多少";
  3. TempIter.next() 打断点,观察 filter 剔除 -99 时游标如何"多走一步"才吐出一个有效值;
  4. 展开 Variables 面板里的 this,对比每次 iterator() 产生的 TempIteri 都是独立从 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。二者都不会因为空序列崩溃——这比很多语言抛异常的设计更安全。


十一、课后练习

  1. 写一个 Countdown <: Iterator<Int64>,从构造参数 n 开始倒着数数到 1,之后返回 None。用 for-in 打印 Countdown(5),应输出 5 4 3 2 1
  2. 给第 1 题的倒计时接上链式操作:只保留偶数、每个数平方、用 forEach 打印。预期输出 16 4(即 4²、2²)。
  3. 写一个无限迭代器 MultiplesOf(k: Int64),依次产出 k 的所有倍数(k、2k、3k……)。用 take(5) 安全地取出 3 的前 5 个倍数并打印。
  4. fold 实现一个函数,接收一个 Iterator<Int64>,返回其中正数的平均值(提示:可以 fold 出一个"个数+总和"的二元组,元组字段用 [0][1] 访问;注意空流除零,用 match 处理)。
  5. 综合:定义一个 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