第 9 章 / 共 58 章 卷二 运算与控制

循环结构

让计算机做它最擅长的事——不知疲倦地重复。

本章导读

第 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 做成员判断。

9.1 while 循环

最通用的循环:条件成立就重复执行循环体,每轮开始前重新检查条件。

countdown.xl
int i = 0
while i < 5
    print(string(i) + " ")
    i += 1
end
println("")
> xl run countdown.xl 0 1 2 3 4

注意形态:while 后面直接跟条件,没有 do、没有 then,换行即进入循环体,end 收尾。习惯 Basic 系语法的读者容易顺手写 while cond do——那是语法错误(见坑 1)。

条件必须是 bool(与 if 相同的纪律),且每轮循环开始前求值一次:条件一开始就不成立,循环体一次都不执行。想让循环"先跑一轮再说",用 9.2 节的 do...while。

循环三要素在 while 里全部手工管理:初始化(int i = 0 在循环外)、条件(i < 5)、推进(i += 1 在循环体内)。忘了推进就是死循环——while 的自由度与风险并存。

9.2 do...while 循环

先执行循环体,再检查条件,条件成立就继续——循环体至少执行一次:

dowhile.xl
int n = 0
do
    n += 10
    println(string(n))
while n < 25 end
println("done")
> xl run dowhile.xl 10 20 30 done

形态要点:do 开头直接换行写循环体;结尾是 while 条件 end,且尾部的 end 必须与条件在同一逻辑行——把 end 单独放到下一行是语法错误(见坑 2)。这是 do...while 最容易踩的形态坑,没有之一。

选型建议:输入校验重试、"先做一次再决定要不要继续"的场景用 do...while;其余默认用 while。上例若改成 while,初始 n=0 时条件已成立,行为相同;但若初始值使条件不成立,while 一轮不跑、do...while 仍会跑一轮——差别就在这一轮。

9.3 for-in 与 range

xlang 的 for 只有一个形态:for 变量 in 序列 ... end。计数循环用内建 range 生成整数序列:

ranges.xl
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("")
> xl run ranges.xl 0 1 2 3 4 2 3 4 5 6 7 0 3 6 9 10 8 6 4 2

四条铁律:

  • stop 永远不包含——range(2, 8) 到 7 为止,与 Python 一致;
  • 步长为负时从 start 倒数到 stop 之上(不含 stop);
  • step 不能为 0,编译期直接报 [sem] range step 不能是 0(见坑 3);
  • 三个参数都可以是变量表达式:range(start, stop) 中的 start/stop 用变量完全合法。

循环变量 i 无需预先声明、不能写类型标注(for int i in ... 是语法错误),它的作用域就是循环体。在循环体内给 i 赋值是允许的,但不会影响迭代——下一轮 i 仍按序列取下一个值(原理见拾遗 2)。

没有三段式 for 会不会不方便?恰恰相反:计数场景被 range 覆盖,遍历场景被 for-in 覆盖,while 兜底一切非常规循环——三件套各司其职,反而不用在 for 括号里塞三个表达式。

9.4 遍历容器

for-in 遍历的不只是 range——数组、字符串、字典都可以直接迭代,还都有"双变量"形态取出位置信息:

iterate.xl
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
> xl run iterate.xl 10 20 30 0: 10 1: 20 2: 30 a b c 0:a 1:b a b a=1 b=2

记忆口诀:数组给元素、字符串给字符、字典给键;加一个变量,数组多索引、字符串多位置、字典多值。遍历字典拿到的键值对顺序按插入序(上例 a 先于 b)。

一个必须知道的限制:for-in 的序列位置不能直接写字面量数组——for x in [10, 20, 30] 能编译,但实测拿到的是无意义的指针值而不是元素(见坑 4,RC0 实现缺陷)。先把数组绑定到变量再遍历,万事大吉。

静态泛型数组 array<int> 的遍历与动态数组一致(单变量拿元素、双变量拿索引),第 16 章容器专题再展开。

9.5 break 与 continue

两个循环控制语句:break 立即退出整个循环,continue 跳过本轮剩余部分、直接进入下一轮:

ctl.xl
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")
> xl run ctl.xl 1 2 4 done

三条纪律:

  • break/continue 必须位于循环体内,否则编译期报 [sem] break/continue 必须位于循环体内——放在 if 里但 if 在循环内是合法的;
  • 作用于最内层循环,xlang 没有带标签的 break(见 9.6 与拾遗 3);
  • while、do...while、for-in 三种循环里行为一致。

while true 加内部 break 是"条件不好一句话说清"时的合法写法:

forever.xl
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 留给退出条件在循环体中段的情形。

9.6 嵌套循环

循环里套循环,经典如九九乘法表:

table.xl
for i in range(1, 4)
    for j in range(1, 4)
        print(string(i) + "x" + string(j) + "=" + string(i * j) + "  ")
    end
    println("")
end
> xl run table.xl 1x1=1 1x2=2 1x3=3 2x1=2 2x2=4 2x3=6 3x1=3 3x2=6 3x3=9

关键语义:内层循环每轮完整跑完,外层才推进一步。break/continue 只作用于直接包裹它的那一层——内层 break 跳出内层,外层继续。

想从双层循环里一次性跳出?xlang 没有 labeled break(break outer 是语法错误),惯用解法有两个:设标志位让外层检查后自行 break;或把内层封装成函数、用 return 一次退到底(第 11 章)。拾遗 3 对此有展开。

9.7 in 成员判断

in 在 for 里是遍历关键字,在表达式里则是成员判断运算符,返回 bool,正好接进第 8 章的 if 条件:

member.xl
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 遍历字典给键的规则一脉相承——字典的世界里,键是第一公民。

guard_in.xl
var config = {"host": "localhost", "port": 8080}
if "host" in config then
    println("host = " + (config["host"] as string))
end
> xl run guard_in.xl host = localhost

"先 in 查键、再索引取值"是处理字典的安全套路,与第 8 章"先 is 后 as"异曲同工。

深入细节

拾遗 1:for-in 与 while 生成的 C 代码长什么样

用 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 被拒绝"都不再是死记的规则,而是这套生成逻辑的直接推论。

拾遗 2:循环变量是"每轮重新绑定"的

在 for i in range(3) 的循环体里写 i = 99,打印三次都是 99,但迭代轮数不受影响——因为每轮迭代开始时,编译器都会把序列的下一个值重新绑定给 i(生成的 C 代码里就是每轮执行一次 i = range_value)。你在循环体内改的只是一个当轮副本。这个设计消除了 C 风格 for 里"循环体内误改计数器导致死循环或跳轮"的整类 bug。想带着状态跨轮演进?用循环外声明的普通变量(如累加器 s),那才归你全权管理。

拾遗 3:为什么没有 labeled break 和带值 break

Java 有 break outer;,Rust 有 break 'outer value;,xlang 目前都没有——break outer 直接语法错误。多层跳出的官方替代是"函数 + return":把内层循环收进函数,return 天然穿透所有循环层,还顺带给了结果一个名字。标志位方案(设 found 让外层自查)依然可用但略显笨拙。语言设计上, labeled break 要求引入标签命名空间和新的语法位,xlang 选择用既有机制(函数)覆盖这个低频需求——这与"没有 switch"的取舍逻辑一致:用更少的语法概念覆盖更多的场景。

⚠️ 常见错误

以下报错全部来自真实编译器输出(文件路径已缩短)。

坑 1:while 后面误加 do
# bad.xl 内容: # int i = 0 # while i < 3 do ← 多了 do # i += 1 # end > xl run bad.xl bad.xl:2:13: [parse] 语法错误

原因:xlang 的 while 没有 do——条件后直接换行。do 只属于 do...while 的开头。报错位置恰好指向那个多余的 do。

坑 2:do...while 的 end 另起一行
# bad.xl 内容: # int n = 0 # do # n += 1 # while n < 3 ← end 被放到下一行 # end > xl run bad.xl bad.xl:5:4: [parse] 语法错误

原因:do...while 的收尾是 while 条件 end,end 必须与条件在同一逻辑行。这是功能清单明文规定的形态约束,也是全语言里少见"换行会出错"的地方,值得单独记忆。

坑 3:range 步长写 0
# bad.xl 内容: # for i in range(1, 10, 0) # println(string(i)) # end > xl run bad.xl bad.xl:1:23: [sem] range step 不能是 0

原因:步长 0 意味着永远原地踏步,必是逻辑错误,编译器在语义检查期直接拦截。想要"每轮重复同一个值"应该换思路:用 while 或干脆不用循环。

坑 4:for-in 直接遍历数组字面量
# bad.xl 内容: # for x in [10, 20, 30] # println(x) # end > xl run bad.xl 12161880 ← 不是 10!是无意义的指针值 12162648 12162744

原因:这是 RC0 版本的实现缺陷——序列位置的数组字面量未经变量绑定,迭代协议拿到的是内部指针而非元素值。不报错、静默出错,是本章最危险的坑。正确做法:auto a = [10, 20, 30]; for x in a ...。先绑定变量,再遍历。

本章小结

知识点写法/规则要点
whilewhile cond ... end先判断后执行;无 do;条件必须 bool
do...whiledo ... 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 判键

复习题

✍️ 先遮住答案想一想
  1. while 与 do...while 的行为差别在什么情况下会显现出来?
  2. range(2, 8) 产生哪些值?range(10, 0, -2) 呢?为什么 stop 始终不包含?
  3. 在 for i in range(10) 循环体里写 i = 100,会影响迭代吗?为什么?
  4. 遍历字典时单变量和双变量分别拿到什么?in dict 判断的又是什么?
  5. 想从双层循环一次性退出,xlang 里有哪些可行方案?
参考答案
  1. 当循环条件一开始就不成立时:while 一轮都不执行,do...while 仍执行一轮。
  2. 2、3、4、5、6、7;10、8、6、4、2。stop 不包含让 range(n) 恰好产生 n 个值(0..n-1),与数组下标天然对齐,区间长度 = stop - start 一目了然。
  3. 不影响轮数与后续取值。循环变量每轮迭代开始时被重新绑定为序列的下一个值,循环体内的修改只作用于当轮副本。
  4. 单变量拿键,双变量拿键和值;in dict 判断键是否存在,与值无关。
  5. 标志位法(外层检查后自行 break);函数法(内层收进函数,用 return 穿透)。xlang 没有 labeled break。

编程练习

练习 1:斐波那契数列

编写 fib.xl:用 while 打印不超过 100 的全部斐波那契数(0 1 1 2 3 5 ... 89)。

参考思路:两个累加变量 a、b 在循环外初始化;每轮打印 a 后同步推进 a, b = b, a+b——xlang 不支持并行赋值,请写成三行(用临时变量或先算后赋)。循环条件是 a <= 100。

练习 2:素数筛

编写 primes.xl:用嵌套 for-in/range 找出 2..50 之间的所有素数并打印。提示:n 是素数当且仅当 2..n-1 中没有它的因数(更优:检查到 n/2 即可)。

参考思路:外层 for n in range(2, 51),内层试除并用标志位记录;体会"内层 break + 标志位"的组合拳。找到因数立即 break 是效率关键。

练习 3:词频统计雏形

编写 freq.xl:给定数组 ["a", "b", "a", "c", "b", "a"],用字典统计每个元素出现次数,最后用 for k, v in 双变量遍历打印 元素=次数。

参考思路:遍历数组,用 9.7 节"先 in 查键、再索引取值"判断字典中是否已有该键:有则计数加一(取出 as int、加一、写回),无则写入 1。这是第 16 章容器专题的预告片。