【仓颉语言入门 · 第5课】循环结构:while、for-in 与 Range

上节课结尾我们留了一个"钩子":想打印 1 到 100,难道手写一百行 println?想把全班每个人的分数都评一遍,难道复制几十段相同的代码?当然不用——循环结构就是让程序"自动重复干活"的机制。

本文是系列第 5 课:正式学习仓颉的三种循环——whiledo-whilefor-in,以及它们最好的搭档 Range(区间),再加上 break / continue 两个流程开关。分支管"选路",循环管"赶路",两者合体,程序才算真正长大。


目录(系列导航)

整套路线共 7 个模块、30 课,按每周 2~3 课的节奏,大约 2~3 个月可以走完一遍:

模块课次内容
一、环境与入门01~05环境搭建与 Hello World、变量与基本类型、运算符与输入输出、分支、循环
二、常用类型与数据组织06~10字符串、数组与区间、ArrayList/HashMap/HashSet、可空类型、错误处理
三、函数与函数式11~14函数、Lambda 与高阶函数、闭包、迭代器与惰性序列
四、面向对象与类型系统15~20struct/class、构造与属性、接口、枚举与 match 模式匹配、泛型、扩展
五、工程化与标准库21~25cjpm 包管理与多文件、文件 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 数据处理程序

一、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..ba..=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 是制表符,负责对齐每一列;实际终端里显示为一整段整齐的表格。)

⚠️ 注意 breakcontinue 只对最靠近它的那一层循环生效:内层里的 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 让程序直接回到读取下一条,counttotal 都不受污染;
  • 最高/最低分的初值是反着取的:最高分初值设得比所有合法值都小(-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 用调试器观察"循环的每一圈"

循环是最能体现调试器威力的结构——同一行代码会执行很多遍,变量每圈都在变:

  1. count++ 这一行左侧行号沟槽点击打断点;
  2. Shift + F9 启动调试,在调试控制台依次输入 8015090-1,每输一个等程序命中断点或继续;
  3. 观察现象:输入 150 时程序没有count++ 处停下(被 continue 提前跳过了),直接回去读下一条;输入 80 时第一次断住;
  4. Variables(变量)面板counttotalhighestlowest:每次按 F8 或调试工具栏的继续按钮跑完一圈再断住时,它们都在变——第二圈 count = 2total = 170lowest = 80
  5. 小实验:把鼠标悬停在代码里的 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 累加变量(如本课的 sumtotal)。

Q4:breakcontinue 到底怎么选?
问自己一句:"是整件事不做了,还是这一次先跳过?"前者用 break(找到了目标、收到了结束信号),后者用 continue(这条数据不合格,但后面还要继续处理)。

Q5:while 和 do-while 什么时候用哪个?
看循环体是否必须至少执行一次。条件可能一开始就不成立、一次都不该跑(余额大于 0 才能继续扣款),用 while;读取输入、显示菜单这类"先做了才知道行不行"的,用 do-while。

Q6:程序好像卡住不动了,怎么办?
八成是死循环:先点 CIDE 结果面板的停止按钮结束程序,再检查三点——计数器有没有初始化、循环体里有没有推进(i++)、推进方向与条件是否一致(条件 i < 10i-- 永远停不下来)。用 while + continue 时还要确认 continue 不会跳过推进语句。

Q7:嵌套循环里的 break 能一次跳出两层吗?
不能,无标签的 break 只结束它所在的最内层循环。初学者可以借助一个标志变量:内层找到目标后置 var found = true 并 break,外层开头判断 if (found) { break }。带标签跳转属于进阶语法,入门阶段不必引入。

Q8:where 和循环体里写 if + continue 有区别吗?
效果等价,都是"不满足条件就不执行循环体"。简单的元素筛选优先用 where,读起来像一句话(“遍历其中满足……的元素”);筛选前后还有别的逻辑要处理时,用 if + continue 更灵活。


九、课后练习

  1. 用 for-in 计算 1~100 中所有偶数的和(两种写法都试:0..101 : 2 步长法、where i % 2 == 0 过滤法),答案应为 2550。
  2. 输入一个正整数 n,计算 n!(阶乘:1 × 2 × … × n)。提示:用 1..=n 遍历,准备一个初值为 1 的 var result 做累乘;顺手算算 5! 应得 120。
  3. 用嵌套循环打印一个 5 行的直角三角形:第 1 行 1 个星号、第 2 行 2 个……同一行的星号用 print("*") 连续输出,每行结束 println() 换行。
  4. 改造 7.1 的成绩程序:录入结束后,除了平均分,再多统计一个"不及格人数"(小于 60 的个数)。准备一个 var failed = 0,想想它该在循环里的什么位置自增。
  5. 打开 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(免费开源、社区版):

Logo

一站式 AI 云服务平台

更多推荐