
【仓颉语言入门 · 第5课】循环结构while、for-in 与 Range上节课结尾我们留了一个钩子想打印 1 到 100难道手写一百行println想把全班每个人的分数都评一遍难道复制几十段相同的代码当然不用——循环结构就是让程序自动重复干活的机制。本文是系列第 5 课正式学习仓颉的三种循环——while、do-while和for-in以及它们最好的搭档Range区间再加上break/continue两个流程开关。分支管选路循环管赶路两者合体程序才算真正长大。目录系列导航整套路线共7 个模块、30 课按每周 23 课的节奏大约 23 个月可以走完一遍模块课次内容一、环境与入门0105环境搭建与 Hello World、变量与基本类型、运算符与输入输出、分支、循环二、常用类型与数据组织0610字符串、数组与区间、ArrayList/HashMap/HashSet、可空类型、错误处理三、函数与函数式1114函数、Lambda 与高阶函数、闭包、迭代器与惰性序列四、面向对象与类型系统1520struct/class、构造与属性、接口、枚举与 match 模式匹配、泛型、扩展五、工程化与标准库2125cjpm 包管理与多文件、文件 IO、JSON 处理、网络编程、单元测试六、并发编程2628线程、Channel 通道与同步原语、并发实战七、项目实战2930命令行小工具、GeoJSON 数据处理实战环境搭建与第一个仓颉程序变量与常量let / var 与基本数据类型运算符与标准输入输出分支结构if 与 match 表达式循环结构while / for / Range本文字符串详解与字符串插值数组 Array 与区间 Range集合框架ArrayList、HashMap、HashSet可空类型?与 Option错误处理异常机制与 Result函数定义、参数与返回值Lambda 表达式与高阶函数闭包、作用域与函数类型迭代器 Iterator 与 Sequence结构体 struct 与类 class构造函数、属性与方法接口 interface 与实现枚举 enum、代数数据类型与 match 模式匹配泛型编程扩展、类型别名与可见性控制cjpm 包管理与多文件项目组织文件与目录 IOJSON 处理结合 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程序的执行过程是i1满足条件就进循环体累加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 11.4 死循环忘了推进的代价三要素里最容易漏的是推进。看看这段代码package helloCangjie main(): Int64 { var i 1 while (i 3) { println(第 ${i} 次循环) i // ← 推进如果删掉这一行i 永远是 1条件永远成立 } println(循环正常结束) return 0 }运行结果第 1 次循环 第 2 次循环 第 3 次循环 循环正常结束⚠️ 假如删掉ii永远停在 1程序会无限打印第 1 次循环——这就是死循环。在 CIDE 里不用慌点结果面板的停止按钮红色方块即可强制结束程序。排查死循环先找两件事计数器有没有推进推进方向和条件是否一致比如条件写i 10却写成了i--另外while (true) { ... }是故意写的无限循环它不是 bug——通常配合后面要学的break在循环体内部决定何时退出本课的综合实战就是这么写的。二、do-while先做一次再检查条件while 是先检查后执行——条件一开始就不成立的话循环体一次都不会跑。但有些事情必须至少做一次比如菜单要先显示、输入要先读到才知道合不合法。仓颉用do-while表达这种先斩后奏do { 先执行一次的循环体 } while (条件) // 做完再检查true 就再来一轮false 才收尾典型场景是输入校验要求用户必须输入 13 之间的选项输错了就让他重输直到合法为止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不包含终点endstart..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、55 也算 print(${i} ) } println() return 0 }运行结果1 2 3 4 1 2 3 4 5for-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人数5for (变量 in 序列)里的变量每轮自动变成序列中的下一个元素完全不用管下标。3.3 步长与倒序: step与负数步长区间默认步长为 1。想隔一个数一个在区间后面加: 步长想倒着数就让起点大于终点、步长写负数package helloCangjie main(): Int64 { for (i in 0..10 : 2) { // 步长 20、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 }运行结果42949672963.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×11 1×22 2×24 1×33 2×36 3×39 1×44 2×48 3×412 4×416 1×55 2×510 3×515 4×520 5×525 1×66 2×612 3×618 4×624 5×630 6×636 1×77 2×714 3×721 4×728 5×735 6×742 7×749 1×88 2×816 3×824 4×832 5×840 6×848 7×856 8×864 1×99 2×918 3×927 4×936 5×945 6×954 7×963 8×972 9×981\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 面板中 count2/total170 的截图。调试结束点停止按钮结束调试会话。八、常见问题 FAQQ1while (n)为什么报错和 if 一样仓颉的条件必须是Bool不做非零即真的隐式转换。写成while (n ! 0)、while (list.size 0)这样的完整条件。Q21..10到底包不包含 10不包含。..是左闭右开区间只数到 9想包含 10 写1..10。反过来10..1因为默认步长是 1、方向矛盾是个空区间一次都不循环也不报错倒序必须写负步长10..1 : -1。Q3for 循环里为什么不能给迭代变量赋值迭代变量是每轮由循环自动绑定的let常量代表当前这一个元素赋值没有意义也容易出错。需要在遍历中累加计算时在循环外另开var累加变量如本课的sum、total。Q4break和continue到底怎么选问自己一句是整件事不做了还是这一次先跳过前者用break找到了目标、收到了结束信号后者用continue这条数据不合格但后面还要继续处理。Q5while 和 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 }。带标签跳转属于进阶语法入门阶段不必引入。Q8where和循环体里写 if continue 有区别吗效果等价都是不满足条件就不执行循环体。简单的元素筛选优先用where读起来像一句话“遍历其中满足……的元素”筛选前后还有别的逻辑要处理时用 if continue 更灵活。九、课后练习用 for-in 计算 1100 中所有偶数的和两种写法都试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 仓颉 SDK1.1.3编写所有代码均已实际编译运行通过。如遇 SDK 版本差异导致的细节出入以你本地版本为准欢迎评论区交流。 工具下载本系列全程使用的仓颉 IDE ——CIDE免费开源、社区版GitCode 仓库 / 安装包下载https://gitcode.com/wp_upala/cide打开页面后进入发行版Releases下载CIDE-版本-x64-Setup.exe安装即可仓颉 SDK 请前往仓颉编程语言官网下载https://cangjie-lang.cn