让计算机做它最擅长的事——不知疲倦地重复。
第 8 章让程序学会"看情况办事",本章让它学会"重复办事"。xlang 的循环家族只有三名成员:while(先判断后执行)、do...while(先执行后判断)、for-in(遍历一个序列)。没有 C 风格的三段式 for (i=0; i<n; i++)——它的生态位被 for i in range(n) 完全接管。
与第 8 章的 if 一样,循环体也用 end 显式收尾;循环条件同样必须是 bool。学完本章,配合容器遍历和 break/continue,你就能写出绝大多数日常循环逻辑。
掌握 while 与 do...while 的形态差异和使用场景;熟练 range 的三种形态与负步长;会用 for-in 遍历数组、字符串、字典并取出索引/键;掌握 break/continue;理解嵌套循环的退出语义;会用 in 做成员判断。
最通用的循环:条件成立就重复执行循环体,每轮开始前重新检查条件。
int i = 0 while i < 5 print(string(i) + " ") i += 1 end println("")
注意形态:while 后面直接跟条件,没有 do、没有 then,换行即进入循环体,end 收尾。习惯 Basic 系语法的读者容易顺手写 while cond do——那是语法错误(见坑 1)。
条件必须是 bool(与 if 相同的纪律),且每轮循环开始前求值一次:条件一开始就不成立,循环体一次都不执行。想让循环"先跑一轮再说",用 9.2 节的 do...while。
循环三要素在 while 里全部手工管理:初始化(int i = 0 在循环外)、条件(i < 5)、推进(i += 1 在循环体内)。忘了推进就是死循环——while 的自由度与风险并存。
先执行循环体,再检查条件,条件成立就继续——循环体至少执行一次:
int n = 0 do n += 10 println(string(n)) while n < 25 end println("done")
形态要点:do 开头直接换行写循环体;结尾是 while 条件 end,且尾部的 end 必须与条件在同一逻辑行——把 end 单独放到下一行是语法错误(见坑 2)。这是 do...while 最容易踩的形态坑,没有之一。
选型建议:输入校验重试、"先做一次再决定要不要继续"的场景用 do...while;其余默认用 while。上例若改成 while,初始 n=0 时条件已成立,行为相同;但若初始值使条件不成立,while 一轮不跑、do...while 仍会跑一轮——差别就在这一轮。
xlang 的 for 只有一个形态:for 变量 in 序列 ... end。计数循环用内建 range 生成整数序列:
for i in range(5) // 形态一:range(stop) → 0..4 print(string(i) + " ") end println("") for i in range(2, 8) // 形态二:range(start, stop) → 2..7 print(string(i) + " ") end println("") for i in range(0, 11, 3) // 形态三:range(start, stop, step) → 0 3 6 9 print(string(i) + " ") end println("") for i in range(10, 0, -2) // 负步长倒数:10 8 6 4 2 print(string(i) + " ") end println("")
四条铁律:
range(2, 8) 到 7 为止,与 Python 一致;[sem] range step 不能是 0(见坑 3);range(start, stop) 中的 start/stop 用变量完全合法。循环变量 i 无需预先声明、不能写类型标注(for int i in ... 是语法错误),它的作用域就是循环体。在循环体内给 i 赋值是允许的,但不会影响迭代——下一轮 i 仍按序列取下一个值(原理见拾遗 2)。
没有三段式 for 会不会不方便?恰恰相反:计数场景被 range 覆盖,遍历场景被 for-in 覆盖,while 兜底一切非常规循环——三件套各司其职,反而不用在 for 括号里塞三个表达式。
for-in 遍历的不只是 range——数组、字符串、字典都可以直接迭代,还都有"双变量"形态取出位置信息:
auto a = [10, 20, 30] for x in a // 数组:单变量拿元素 println(x) end for i, x in a // 数组:双变量拿索引 + 元素 println(string(i) + ": " + string(x)) end for c in "abc" // 字符串:逐字符 println(string(c)) end for i, c in "ab" // 字符串:索引 + 字符 println(string(i) + ":" + string(c)) end var d = {"a": 1, "b": 2} for k in d // 字典:单变量拿键 println(string(k)) end for k, v in d // 字典:双变量拿键 + 值 println(string(k) + "=" + string(v)) end
记忆口诀:数组给元素、字符串给字符、字典给键;加一个变量,数组多索引、字符串多位置、字典多值。遍历字典拿到的键值对顺序按插入序(上例 a 先于 b)。
一个必须知道的限制:for-in 的序列位置不能直接写字面量数组——for x in [10, 20, 30] 能编译,但实测拿到的是无意义的指针值而不是元素(见坑 4,RC0 实现缺陷)。先把数组绑定到变量再遍历,万事大吉。
静态泛型数组 array<int> 的遍历与动态数组一致(单变量拿元素、双变量拿索引),第 16 章容器专题再展开。
两个循环控制语句:break 立即退出整个循环,continue 跳过本轮剩余部分、直接进入下一轮:
for i in range(1, 6) if i == 3 then continue // 跳过 3 end if i == 5 then break // 到 5 就整体退出 end print(string(i) + " ") end println("done")
三条纪律:
[sem] break/continue 必须位于循环体内——放在 if 里但 if 在循环内是合法的;while true 加内部 break 是"条件不好一句话说清"时的合法写法:
int n = 0 while true n += 1 if n >= 3 then break end end println(string(n)) // 3
注意这等价于 do...while 的"先执行后判断"——能用 do...while 说清的场景优先用它,while true + break 留给退出条件在循环体中段的情形。
循环里套循环,经典如九九乘法表:
for i in range(1, 4) for j in range(1, 4) print(string(i) + "x" + string(j) + "=" + string(i * j) + " ") end println("") end
关键语义:内层循环每轮完整跑完,外层才推进一步。break/continue 只作用于直接包裹它的那一层——内层 break 跳出内层,外层继续。
想从双层循环里一次性跳出?xlang 没有 labeled break(break outer 是语法错误),惯用解法有两个:设标志位让外层检查后自行 break;或把内层封装成函数、用 return 一次退到底(第 11 章)。拾遗 3 对此有展开。
in 在 for 里是遍历关键字,在表达式里则是成员判断运算符,返回 bool,正好接进第 8 章的 if 条件:
println(3 in range(1, 10)) // true:range 按区间语义判断 println(30 in range(1, 10)) // false println("b" in "abc") // true:子串包含 println(2 in [1, 2, 3]) // true:数组元素包含 var d = {"k": 1} println("k" in d) // true:字典按键判断
注意两个分工:in range(...) 判断的是区间归属(按起止和步长语义,不需要真的展开序列);in dict 判断的是键是否存在,与值无关。这与 for-in 遍历字典给键的规则一脉相承——字典的世界里,键是第一公民。
var config = {"host": "localhost", "port": 8080} if "host" in config then println("host = " + (config["host"] as string)) end
"先 in 查键、再索引取值"是处理字典的安全套路,与第 8 章"先 is 后 as"异曲同工。
用 xl cgen 观察代码生成:for i in range(5) 会被展开成一个地道的 C for 循环——start/stop/step 先求值存入局部变量,循环头用一个三元表达式按步长正负动态选择比较方向(step > 0 ? value < stop : value > stop),这就是为什么负步长"天然就灵"。而 while i < 5 生成的是 while (1) 加循环体开头一句 if (!(i < 5)) break;。理解这层展开后,"stop 不包含""step 为 0 被拒绝"都不再是死记的规则,而是这套生成逻辑的直接推论。
在 for i in range(3) 的循环体里写 i = 99,打印三次都是 99,但迭代轮数不受影响——因为每轮迭代开始时,编译器都会把序列的下一个值重新绑定给 i(生成的 C 代码里就是每轮执行一次 i = range_value)。你在循环体内改的只是一个当轮副本。这个设计消除了 C 风格 for 里"循环体内误改计数器导致死循环或跳轮"的整类 bug。想带着状态跨轮演进?用循环外声明的普通变量(如累加器 s),那才归你全权管理。
Java 有 break outer;,Rust 有 break 'outer value;,xlang 目前都没有——break outer 直接语法错误。多层跳出的官方替代是"函数 + return":把内层循环收进函数,return 天然穿透所有循环层,还顺带给了结果一个名字。标志位方案(设 found 让外层自查)依然可用但略显笨拙。语言设计上, labeled break 要求引入标签命名空间和新的语法位,xlang 选择用既有机制(函数)覆盖这个低频需求——这与"没有 switch"的取舍逻辑一致:用更少的语法概念覆盖更多的场景。
以下报错全部来自真实编译器输出(文件路径已缩短)。
原因:xlang 的 while 没有 do——条件后直接换行。do 只属于 do...while 的开头。报错位置恰好指向那个多余的 do。
原因:do...while 的收尾是 while 条件 end,end 必须与条件在同一逻辑行。这是功能清单明文规定的形态约束,也是全语言里少见"换行会出错"的地方,值得单独记忆。
原因:步长 0 意味着永远原地踏步,必是逻辑错误,编译器在语义检查期直接拦截。想要"每轮重复同一个值"应该换思路:用 while 或干脆不用循环。
原因:这是 RC0 版本的实现缺陷——序列位置的数组字面量未经变量绑定,迭代协议拿到的是内部指针而非元素值。不报错、静默出错,是本章最危险的坑。正确做法:auto a = [10, 20, 30]; for x in a ...。先绑定变量,再遍历。
| 知识点 | 写法/规则 | 要点 |
|---|---|---|
| while | while cond ... end | 先判断后执行;无 do;条件必须 bool |
| do...while | do ... while cond end | 至少执行一次;end 与条件同一逻辑行 |
| range 三形态 | range(stop) / range(start,stop) / range(start,stop,step) | stop 不含;step 可负不可 0;参数可用变量 |
| 遍历数组 | for x in a / for i, x in a | 元素 / 索引+元素;须先绑定变量 |
| 遍历字符串 | for c in s / for i, c in s | 逐字符 / 位置+字符 |
| 遍历字典 | for k in d / for k, v in d | 键 / 键+值 |
| break/continue | 最内层循环 | 循环体外 [sem] 报错;无标签 break |
| in 判断 | x in range(...) k in dict | 返回 bool;range 判区间、dict 判键 |
range(2, 8) 产生哪些值?range(10, 0, -2) 呢?为什么 stop 始终不包含?for i in range(10) 循环体里写 i = 100,会影响迭代吗?为什么?in dict 判断的又是什么?range(n) 恰好产生 n 个值(0..n-1),与数组下标天然对齐,区间长度 = stop - start 一目了然。in dict 判断键是否存在,与值无关。编写 fib.xl:用 while 打印不超过 100 的全部斐波那契数(0 1 1 2 3 5 ... 89)。
参考思路:两个累加变量 a、b 在循环外初始化;每轮打印 a 后同步推进 a, b = b, a+b——xlang 不支持并行赋值,请写成三行(用临时变量或先算后赋)。循环条件是 a <= 100。
编写 primes.xl:用嵌套 for-in/range 找出 2..50 之间的所有素数并打印。提示:n 是素数当且仅当 2..n-1 中没有它的因数(更优:检查到 n/2 即可)。
参考思路:外层 for n in range(2, 51),内层试除并用标志位记录;体会"内层 break + 标志位"的组合拳。找到因数立即 break 是效率关键。
编写 freq.xl:给定数组 ["a", "b", "a", "c", "b", "a"],用字典统计每个元素出现次数,最后用 for k, v in 双变量遍历打印 元素=次数。
参考思路:遍历数组,用 9.7 节"先 in 查键、再索引取值"判断字典中是否已有该键:有则计数加一(取出 as int、加一、写回),无则写入 1。这是第 16 章容器专题的预告片。