【仓颉语言入门 · 第5课】
【仓颉语言入门 · 第5课】循环结构:while、for-in 与 Range
上节课结尾我们留了一个"钩子":想打印 1 到 100,难道手写一百行
println?想把全班每个人的分数都评一遍,难道复制几十段相同的代码?当然不用——循环结构就是让程序"自动重复干活"的机制。本文是系列第 5 课:正式学习仓颉的三种循环——while、do-while 和 for-in,以及它们最好的搭档 Range(区间),再加上
break/continue两个流程开关。分支管"选路",循环管"赶路",两者合体,程序才算真正长大。
目录(系列导航)
整套路线共 7 个模块、30 课,按每周 2~3 课的节奏,大约 2~3 个月可以走完一遍:
| 模块 | 课次 | 内容 |
|---|---|---|
| 一、环境与入门 | 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 数据处理程序
一、while 循环:满足条件就一直重复
1.1 先跑一段整体感受一下
不用循环求 1 到 100 的和,得写一长串 1 + 2 + 3 + ...。用 while 只要 6 行:
package helloCangjie
main(): Int64 {
var i = 1 // ① 起点:计数器从 1 开始
var sum = 0
while (i <= 100) { // ② 条件:只要 i 没超过 100 就继续
sum += i // 把当前的 i 累加进 sum
i++ // ③ 推进:i 每次增加 1,一步步走向终点
}
println("1 到 100 的和是 ${sum}")
return 0
}
运行结果:
1 到 100 的和是 5050
程序的执行过程是:i=1 满足条件就进循环体累加,i 变成 2;再判断、再加……直到 i 变成 101,条件 101 <= 100 不成立,循环结束。同一段循环体被执行了整整 100 次,代码却只有几行。
1.2 语法要点:循环三要素
while (条件) {
条件为 true 时反复执行的循环体
}
和 if 一样,条件必须写在小括号里、循环体必须用大括号包住(只有一条语句也不能省)。写 while 时脑子里要盯住循环三要素,缺一不可:
| 要素 | 作用 | 本例 |
|---|---|---|
| 起点(初始化) | 循环开始前计数器是多少 | var i = 1 |
| 条件(守门) | 什么时候继续、什么时候停 | i <= 100 |
| 推进(更新) | 每轮让状态朝终点迈一步 | i++ |
三要素配合起来才能保证循环"干得完活也收得了场"。
1.3 条件仍然必须是 Bool
第 4 课那条铁律继续生效:while 的条件只认 Bool,不搞"非零即真":
package helloCangjie
main(): Int64 {
var n = 3
// while (n) { } // 编译错误:条件必须是 Bool
while (n != 0) { // 正确:明确写出比较
println("n = ${n}")
n--
}
return 0
}
运行结果:
n = 3
n = 2
n = 1
1.4 死循环:忘了"推进"的代价
三要素里最容易漏的是推进。看看这段代码:
package helloCangjie
main(): Int64 {
var i = 1
while (i <= 3) {
println("第 ${i} 次循环")
i++ // ← 推进!如果删掉这一行,i 永远是 1,条件永远成立
}
println("循环正常结束")
return 0
}
运行结果:
第 1 次循环
第 2 次循环
第 3 次循环
循环正常结束
⚠️ 假如删掉 i++,i 永远停在 1,程序会无限打印"第 1 次循环"——这就是死循环。在 CIDE 里不用慌,点结果面板的停止按钮(红色方块)即可强制结束程序。排查死循环先找两件事:计数器有没有推进?推进方向和条件是否一致(比如条件写 i < 10,却写成了 i--)?
另外,while (true) { ... } 是故意写的"无限循环",它不是 bug——通常配合后面要学的 break,在循环体内部决定何时退出(本课的综合实战就是这么写的)。
二、do-while:先做一次,再检查条件
while 是"先检查,后执行"——条件一开始就不成立的话,循环体一次都不会跑。但有些事情必须至少做一次:比如菜单要先显示、输入要先读到,才知道合不合法。仓颉用 do-while 表达这种"先斩后奏":
do {
先执行一次的循环体
} while (条件) // 做完再检查:true 就再来一轮,false 才收尾
典型场景是输入校验:要求用户必须输入 1~3 之间的选项,输错了就让他重输,直到合法为止:
package helloCangjie
import std.env.*
import std.convert.*
main(): Int64 {
var choice: Int64 = 0
do {
print("请输入 1~3 之间的选项:")
choice = Int64.parse(getStdIn().readln().getOrThrow())
} while (choice < 1 || choice > 3)
println("你选择了 ${choice}")
return 0
}
按 Ctrl + R 运行,故意先输错两次:
请输入 1~3 之间的选项:9
请输入 1~3 之间的选项:0
请输入 1~3 之间的选项:2
你选择了 2
注意体会和 while 的区别:哪怕第一次就输入合法值,读取动作也已经发生了——do-while 的循环体保证执行 1 次起步;如果校验逻辑放在普通 while 前面,就得把"提示 + 读取"的代码重复写两遍。
三、for-in 与 Range:数数循环的正规军
while 很自由,但"从 1 数到 100"这种次数明确的循环,每次都手写"初始化 + 条件 + i++"三件套还是太啰嗦。仓颉的 for-in 配合 Range(区间) 把三件套一次写齐。
3.1 两种区间:a..b 与 a..=b
仓颉用两个点表示一段连续整数:
start..end:左闭右开——包含起点start,不包含终点end;start..=end:左闭右闭——起点和终点都包含。
package helloCangjie
main(): Int64 {
for (i in 1..5) { // 1..5:数 1、2、3、4,到 5 之前停
print("${i} ")
}
println()
for (i in 1..=5) { // 1..=5:数 1、2、3、4、5,5 也算
print("${i} ")
}
println()
return 0
}
运行结果:
1 2 3 4
1 2 3 4 5
for-in 的读法是:“让 i 依次取遍区间里的每个值,每取一个就执行一遍循环体”。计数器 i 不用声明、不用自增,全部由 for 自动打理,想漏写推进都没机会。
💡 记忆技巧:
..右边少了一横,就像终点处缺了一块——终点取不到;..=补全了一横,终点稳稳包含。数数循环里"数 n 次"最常用的写法是0..n(从 0 开始数 n 个)和1..=n(从 1 数到 n)。
3.2 for-in 直接遍历数组
for-in 不只配区间,任何"一串元素"都能遍历,比如直接遍历数组(数组第 7 课细讲,这里先混个眼熟):
package helloCangjie
main(): Int64 {
let scores = [86, 92, 57, 73, 100]
var total = 0
for (score in scores) {
println("读到成绩:${score}")
total += score
}
println("总分:${total},人数:${scores.size}")
return 0
}
运行结果:
读到成绩:86
读到成绩:92
读到成绩:57
读到成绩:73
读到成绩:100
总分:408,人数:5
for (变量 in 序列) 里的变量每轮自动变成序列中的下一个元素,完全不用管下标。
3.3 步长与倒序:: step 与负数步长
区间默认步长为 1。想"隔一个数一个",在区间后面加 : 步长;想倒着数,就让起点大于终点、步长写负数:
package helloCangjie
main(): Int64 {
for (i in 0..10 : 2) { // 步长 2:0、2、4、6、8(半开,不含 10)
print("${i} ")
}
println()
for (i in 10..1 : -1) { // 步长 -1:从 10 倒数到 2(半开,不含 1)
print("${i} ")
}
println()
for (i in 10..=0 : -2) { // 步长 -2 且闭区间:10、8、6、4、2、0
print("${i} ")
}
println()
return 0
}
运行结果:
0 2 4 6 8
10 9 8 7 6 5 4 3 2
10 8 6 4 2 0
⚠️ 步长方向和区间方向必须一致,否则会得到一个空区间,循环体一次都不执行(这不是报错,是合法的"空序列"):
package helloCangjie
main(): Int64 {
var rounds = 0
for (i in 10..1) { // 默认步长 +1,却想从大数走向小数 → 区间为空
rounds++
}
println("循环执行了 ${rounds} 轮") // 0 轮
return 0
}
倒序请老老实实写上负步长 10..1 : -1。
3.4 迭代变量不能改;用不到就写 _
for 的迭代变量本质上是 let 常量,循环体里不许重新赋值:
package helloCangjie
main(): Int64 {
// for (i in 1..=5) {
// i = i * 10 // 编译错误:迭代变量是 let 常量,不能赋值
// }
// 正确做法:需要"边遍历边算"时,另开一个累加变量
var doubledSum = 0
for (i in 1..=5) {
doubledSum += i * 10
}
println("结果:${doubledSum}") // 150
return 0
}
如果只想"重复执行 N 次"、根本不关心数到几,可以用通配符 _ 占位,还能避免"变量未使用"的告警:
package helloCangjie
main(): Int64 {
var number = 2
for (_ in 0..5) { // 重复 5 轮:2 → 4 → 16 → 256 → 65536 → 4294967296
number *= number
}
println(number)
return 0
}
运行结果:
4294967296
3.5 where:循环门口的"过滤器"
只想遍历其中一部分值(比如只要奇数)时,可以在序列后加 where 条件:每轮进入循环体之前先过一遍筛子,条件为 true 才执行,否则直接进入下一轮:
package helloCangjie
main(): Int64 {
for (i in 0..8 where i % 2 == 1) {
print("${i} ")
}
println()
return 0
}
运行结果:
1 3 5 7
它等价于在循环体开头写 if (条件不满足) { continue }(下一节就讲),但写在 for 头上意图更直白:“遍历满足条件的元素”。
四、break 与 continue:提前离场 / 跳过本轮
两个开关都写在循环体内部,作用截然不同:
| 关键字 | 行为 | 生活类比 |
|---|---|---|
| break | 立刻结束整个循环,跳到循环之后继续执行 | 电影院找座位,找到自己的位置后直接离场,不再往后找 |
| continue | 只结束本轮,跳过本轮剩余语句,直接进入下一轮条件判断 | 发传单时这人摆手不要,跳过他,继续发给下一个人 |
break 的经典用法——在数组中找第一个能被 5 整除的数,找到就收工:
package helloCangjie
main(): Int64 {
let numbers = [12, 18, 25, 36, 49, 55]
for (number in numbers) {
if (number % 5 == 0) {
println("找到了:${number}")
break // 任务完成,后面的 36、49、55 统统不看了
}
}
println("查找结束")
return 0
}
运行结果:
找到了:25
查找结束
continue 的经典用法——把数组里的奇数挑出来打印,偶数直接跳过:
package helloCangjie
main(): Int64 {
let numbers = [12, 18, 25, 36, 49, 55]
for (number in numbers) {
if (number % 2 == 0) {
continue // 本轮是偶数,println 被跳过,直接看下一个数
}
println(number)
}
return 0
}
运行结果:
25
49
55
💡 while 循环里用
continue要格外小心:continue跳过本轮剩余语句时,如果计数器的i++恰好写在被跳过的后半段,计数器就永远不推进,又会死循环。稳妥做法是把推进语句放在循环体开头,或者改用 for-in——它的推进由循环自动完成,continue不会影响计数。
五、嵌套循环:循环里还能套循环
循环体里可以再放循环,形成嵌套循环。外层走一步,内层完整跑一遍。最经典的练习是九九乘法表:外层变量 i 是"第几行"(被乘数),内层变量 j 是"这一行的第几列"(乘数),内层走完打个换行:
package helloCangjie
main(): Int64 {
for (i in 1..=9) {
for (j in 1..=i) {
print("${j}×${i}=${i * j}\t")
}
println() // 内层跑完一整行,换行
}
return 0
}
运行结果:
1×1=1
1×2=2 2×2=4
1×3=3 2×3=6 3×3=9
1×4=4 2×4=8 3×4=12 4×4=16
1×5=5 2×5=10 3×5=15 4×5=20 5×5=25
1×6=6 2×6=12 3×6=18 4×6=24 5×6=30 6×6=36
1×7=7 2×7=14 3×7=21 4×7=28 5×7=35 6×7=42 7×7=49
1×8=8 2×8=16 3×8=24 4×8=32 5×8=40 6×8=48 7×8=56 8×8=64
1×9=9 2×9=18 3×9=27 4×9=36 5×9=45 6×9=54 7×9=63 8×9=72 9×9=81
(\t 是制表符,负责对齐每一列;实际终端里显示为一整段整齐的表格。)
⚠️ 注意 break 和 continue 只对最靠近它的那一层循环生效:内层里的 break 只能跳出内层,外层还会继续。初学阶段知道这个边界即可,带标签的跨层跳转等以后遇到真实场景再学。
六、三种循环怎么选
| 场景 | 推荐 |
|---|---|
| 次数明确、就是"从某数到某数" | for (i in a..=b) + Range |
| 遍历一组已经存在的数据(数组等) | for (x in 序列) |
| 遍历时只想保留满足条件的元素 | for-in + where |
| 循环次数事先不知道,靠运行时条件决定停不停(读到哨兵值、猜对数) | while |
| 循环体必须至少执行一次(菜单显示、输入校验) | do-while |
一句话:数得清次数用 for,数不清用 while,先做后问用 do-while。
七、CIDE 实操:成绩录入统计程序(综合实战)
综合本课知识:循环录入一批成绩(个数事先不知道),输入 -1 表示结束;中途输错范围(如 150)要能忽略后继续;结束后输出人数、总分、平均分、最高/最低分。
7.1 编写程序
继续使用 helloCangjie 项目(也可 Ctrl + N 新建),把 src/main.cj 替换为:
package helloCangjie
import std.env.*
import std.convert.*
main(): Int64 {
var count = 0
var total = 0
var highest = -1 // 最高分的初始值取一个"比任何合法成绩都小"的数
var lowest = 101 // 最低分的初始值取一个"比任何合法成绩都大"的数
// 次数未知 → while(true) 无限循环,靠内部的 break 收尾
while (true) {
print("请输入成绩(0~100,输入 -1 结束):")
let score = Int64.parse(getStdIn().readln().getOrThrow())
if (score == -1) {
break // 哨兵值:结束录入
}
if (score < 0 || score > 100) {
println(" 成绩超出 0~100 范围,已忽略")
continue // 本轮数据作废,直接进入下一轮录入
}
count++
total += score
if (score > highest) {
highest = score
}
if (score < lowest) {
lowest = score
}
}
if (count == 0) {
println("没有录入任何有效成绩。")
return 0
}
let average = Float64(total) / Float64(count)
println("------ 统计结果 ------")
println("有效人数:${count}")
println("总分:${total}")
println("平均分:${average}")
println("最高分:${highest}")
println("最低分:${lowest}")
return 0
}
写完按 Ctrl + Alt + L 格式化。几个设计要点:
- 哨兵循环:录入个数事先不知道,没法用 for 数次数。约定一个特殊值
-1当"结束信号"(哨兵),用while (true)+break收尾; - continue 兜底脏数据:超范围的成绩不参与统计,
continue让程序直接回到读取下一条,count、total都不受污染; - 最高/最低分的初值是反着取的:最高分初值设得比所有合法值都小(
-1),第一个合法成绩必然比它大从而完成替换;最低分同理取101。
7.2 运行与验证
按 Ctrl + R 运行,输入 85 92 77 150 60 -1(每输一个按一次回车,故意混入一个 150 测试 continue):
请输入成绩(0~100,输入 -1 结束):85
请输入成绩(0~100,输入 -1 结束):92
请输入成绩(0~100,输入 -1 结束):77
请输入成绩(0~100,输入 -1 结束):150
成绩超出 0~100 范围,已忽略
请输入成绩(0~100,输入 -1 结束):60
请输入成绩(0~100,输入 -1 结束):-1
------ 统计结果 ------
有效人数:4
总分:314
平均分:78.500000
最高分:92
最低分:60
再测一种边界情况:一上来直接输入 -1,程序应礼貌退出而不是算出奇怪的平均值:
请输入成绩(0~100,输入 -1 结束):-1
没有录入任何有效成绩。
💡
平均分:78.500000有 6 位小数,这是Float64浮点数的默认显示格式(314 ÷ 4 = 78.5,后面补零)。想要"保留 1 位小数"的格式化输出,等学到标准库格式化章节再优化,现阶段认识到"浮点除法结果带小数、整数除法total / count会丢掉小数部分"即可。
📸 配图建议:Result 面板完整显示含非法输入被忽略、最终统计输出的运行截图。
7.3 用调试器观察"循环的每一圈"
循环是最能体现调试器威力的结构——同一行代码会执行很多遍,变量每圈都在变:
- 在
count++这一行左侧行号沟槽点击打断点; - 按 Shift + F9 启动调试,在调试控制台依次输入
80、150、90、-1,每输一个等程序命中断点或继续; - 观察现象:输入
150时程序没有在count++处停下(被continue提前跳过了),直接回去读下一条;输入80时第一次断住; - 在 Variables(变量)面板看
count、total、highest、lowest:每次按 F8 或调试工具栏的继续按钮跑完一圈再断住时,它们都在变——第二圈count = 2、total = 170、lowest = 80; - 小实验:把鼠标悬停在代码里的
score上,逐圈确认当前录入的值;输入-1后观察程序如何走break一路跳到统计输出。
📸 配图建议:断点在 count++ 处第二次命中时,Variables 面板中 count=2/total=170 的截图。
调试结束点停止按钮,结束调试会话。
八、常见问题 FAQ
Q1:while (n) 为什么报错?
和 if 一样,仓颉的条件必须是 Bool,不做"非零即真"的隐式转换。写成 while (n != 0)、while (list.size > 0) 这样的完整条件。
Q2:1..10 到底包不包含 10?
不包含。.. 是左闭右开区间,只数到 9;想包含 10 写 1..=10。反过来,10..1 因为默认步长是 +1、方向矛盾,是个空区间(一次都不循环,也不报错);倒序必须写负步长 10..1 : -1。
Q3:for 循环里为什么不能给迭代变量赋值?
迭代变量是每轮由循环自动绑定的 let 常量,代表"当前这一个元素",赋值没有意义也容易出错。需要在遍历中累加计算时,在循环外另开 var 累加变量(如本课的 sum、total)。
Q4:break 和 continue 到底怎么选?
问自己一句:"是整件事不做了,还是这一次先跳过?"前者用 break(找到了目标、收到了结束信号),后者用 continue(这条数据不合格,但后面还要继续处理)。
Q5:while 和 do-while 什么时候用哪个?
看循环体是否必须至少执行一次。条件可能一开始就不成立、一次都不该跑(余额大于 0 才能继续扣款),用 while;读取输入、显示菜单这类"先做了才知道行不行"的,用 do-while。
Q6:程序好像卡住不动了,怎么办?
八成是死循环:先点 CIDE 结果面板的停止按钮结束程序,再检查三点——计数器有没有初始化、循环体里有没有推进(i++)、推进方向与条件是否一致(条件 i < 10 配 i-- 永远停不下来)。用 while + continue 时还要确认 continue 不会跳过推进语句。
Q7:嵌套循环里的 break 能一次跳出两层吗?
不能,无标签的 break 只结束它所在的最内层循环。初学者可以借助一个标志变量:内层找到目标后置 var found = true 并 break,外层开头判断 if (found) { break }。带标签跳转属于进阶语法,入门阶段不必引入。
Q8:where 和循环体里写 if + continue 有区别吗?
效果等价,都是"不满足条件就不执行循环体"。简单的元素筛选优先用 where,读起来像一句话(“遍历其中满足……的元素”);筛选前后还有别的逻辑要处理时,用 if + continue 更灵活。
九、课后练习
- 用 for-in 计算 1~100 中所有偶数的和(两种写法都试:
0..101 : 2步长法、where i % 2 == 0过滤法),答案应为 2550。 - 输入一个正整数
n,计算n!(阶乘:1 × 2 × … × n)。提示:用1..=n遍历,准备一个初值为 1 的var result做累乘;顺手算算5!应得 120。 - 用嵌套循环打印一个 5 行的直角三角形:第 1 行 1 个星号、第 2 行 2 个……同一行的星号用
print("*")连续输出,每行结束println()换行。 - 改造 7.1 的成绩程序:录入结束后,除了平均分,再多统计一个"不及格人数"(小于 60 的个数)。准备一个
var failed = 0,想想它该在循环里的什么位置自增。 - 打开 7.1 的程序 Shift + F9 调试,在
total += score一行打断点,输入70 80 -1,用 F8 逐圈单步,在 Variables 面板观察total从 70 变到 150 的过程;再重新调试输入200 -1,观察continue如何跳过count++与total += score。
下节预告
循环这一关一过,你已经掌握了程序三大流程控制(顺序、分支、循环)的全部基本功。前几课我们一直在和数字、布尔值打交道,但程序处理得最多的其实是文本:怎么拼接两段文字、怎么取出其中一部分、怎么把数字变成字符串嵌进一句话里?第 6 课我们学习字符串详解与字符串插值,让程序开始"好好说话"。我们下节课见!
系列说明:本系列基于 Windows 平台 + CIDE + 仓颉 SDK(1.1.3)编写,所有代码均已实际编译运行通过。如遇 SDK 版本差异导致的细节出入,以你本地版本为准,欢迎评论区交流。
📥 工具下载
本系列全程使用的仓颉 IDE —— CIDE(免费开源、社区版):
- GitCode 仓库 / 安装包下载:https://gitcode.com/wp_upala/cide
- 打开页面后进入 发行版(Releases),下载
CIDE-<版本>-x64-Setup.exe安装即可 - 仓颉 SDK 请前往仓颉编程语言官网下载:https://cangjie-lang.cn
更多推荐




所有评论(0)