大家好,我是[晚风依旧似温柔],新人一枚,欢迎大家关注~

1. 设计视角:为什么仓颉把循环也当“表达式”看待?

很多传统语言把 for/while 归类为“语句(statement)”。仓颉的不同之处在于:凡是可求值的语言元素都是表达式,包括条件与循环,这些表达式都有明确的类型和值(循环表达式的类型为 Unit、值为 ())——这让控制流与求值规则可以自然融入类型系统,既便于组合,也便于推导与检查。这样带来的直接好处是:控制流更“函数式”,可与其它表达式自由拼接,减少“只能写语句、不能嵌入表达式”的割裂感。(docs.cangjie-lang.cn)

更进一步,仓颉把循环细分为四种:for-inwhiledo-whilewhile-let。其中 for-in 专注可迭代数据while / do-while 面向基于条件的过程控制,而 while-let 则把模式匹配与循环结合,在处理可能失败/可选值(如 Option<T>)的连续拉取等场景里极具表达力。(docs.cangjie-lang.cn)

2. 语法与要点总览

2.1 while:以条件为中心的迭代

  • 语义:先判定条件,再执行循环体;条件为 false 时结束。
  • 适用:不确定迭代次数、需要明确的收敛/退出条件(容忍度、上界、状态变化)等数值/过程型任务。
  • 关键点:条件必须是布尔表达式;循环体是代码块。(docs.cangjie-lang.cn)

示例(数值逼近)
(见“实战 A”中的牛顿法/二分法片段)

2.2 do-while:至少执行一次

  • 语义:先执行循环体,再判定是否继续。
  • 适用:至少执行一次的流程,如“先读取一次、再决定是否继续读取”或“先显示菜单、再判断是否退出”。(docs.cangjie-lang.cn)

示例(蒙特卡洛 π)
(见“实战 A”中的 π 估算片段)

2.3 for-in:遍历一切可迭代对象

  • 语义:遍历实现了 Iterable<T> 接口的序列(数组、区间等);支持元组解构占位符 _where 条件过滤(进入循环体前即判断)。
  • 适用:集合/序列处理的首选;where 可在进入循环体前进行布尔裁剪,减少分支与 continue。(docs.cangjie-lang.cn)

要点:

  1. “序列”表达式只求值一次
  2. 迭代变量不可修改
  3. 可对元组元素直接解构;
  4. where 让过滤逻辑更直观。(docs.cangjie-lang.cn)

示例(区间、解构、where)
(见“实战 B”与“实战 C”)

2.4 while-let:把“取值+匹配”折叠进循环条件

  • 语义:对 <- 右侧表达式求值,与 <- 左侧的模式匹配;匹配成功则执行循环体并继续,否则结束。
  • 适用:流式拉取可能失败的读取迭代器返回 Option<T>/枚举 的场景,避免“先读—判空—再继续”的样板代码。(docs.cangjie-lang.cn)

3. 选择指南:谁更适合你的问题?

  • 遍历容器/区间 → for-in:带来更清晰的意图表达与更少的错误面。where 在进入循环体前就裁剪数据,避免无谓分支。(docs.cangjie-lang.cn)
  • 数值逼近/过程控制 → while:与你的收敛条件同构,关注不变量、单调性与误差上界。(docs.cangjie-lang.cn)
  • 至少执行一次 → do-while:典型如“先做一次 I/O 或 UI 交互,再判断是否继续”。(docs.cangjie-lang.cn)
  • 可选值/枚举驱动的拉取式循环 → while-let:将“取值 + 解构/匹配 + 判断”合一,显著降噪,也更安全(不易遗漏失败分支)。(docs.cangjie-lang.cn)

4. 实战 A:数值计算中的循环策略(精度、收敛与鲁棒性)

目标:实现两个经典计算——(1)二分法/牛顿法求平方根;(2)蒙特卡洛估算 π。我们关注循环退出条件数值稳定性可读性

4.1 二分法与 while:把“误差小于阈值”写进条件

优势在于:停止条件即代码意图,循环内只做收缩区间的纯逻辑。仓颉官方文档中给出了以 while 表达式近似计算 √2 的示例,核心是把误差平方与阈值放进条件,让退出条件可见且可调。(docs.cangjie-lang.cn)

main() {
    var min = 1.0
    var max = 2.0
    var root = 0.0
    var error = 1.0
    let tol = 1e-10  // 容忍度

    while (error * error > tol) {
        root = (min + max) / 2.0
        error = root * root - 2.0
        if (error > 0.0) { max = root } else { min = root }
    }
    println("sqrt(2) ≈ ${root}")
}

工程思考

  • 容忍度与溢出:用 error * error > tol 而非 abs(error) > tol 可以避免引入额外库,且对称;若担心数值上下溢,可在外部包一层 abs
  • 单调性保证:二分法依赖有界、单峰/单根与连续性;对更复杂函数,需在循环外预先定位区间,否则会“卡死”或过早收敛。
  • 不可变 vs 可变:这里选择可变变量以减少中间对象;若偏函数式,可把状态封装到小记录体里,返回新状态。

4.2 do-while 与蒙特卡洛:至少跑一次采样

官方文档示例展示了用 do-while 进行 π 的蒙特卡洛近似,先采样、后判断,直观表达“先做一次”的业务语义。(docs.cangjie-lang.cn)

import std.random.*

main() {
    let rnd = Random()
    var total = 0
    var hit = 0
    do {
        let x = rnd.nextFloat64()
        let y = rnd.nextFloat64()
        if ((x - 0.5) ** 2 + (y - 0.5) ** 2 < 0.25) { hit++ }
        total++
    } while (total < 1_000_000)

    let pi = 4.0 * Float64(hit) / Float64(total)
    println("π ≈ ${pi}")
}

工程思考

  • 采样独立性:将 Random 实例提到循环外,避免每轮重新构造影响分布。
  • 退出上界:Monte Carlo 无法以“误差达到阈值”确定退出,通常按批次/上界退出;如果要动态估计方差,则可在固定批次后评估置信区间再决定是否继续。

5. 实战 B:for-in 的“可读性增强包”——区间、解构与 where

5.1 遍历区间与累加

for-in 可直接遍历区间(内置类型已扩展 Iterable<T>),表达“从 1 到 100 的和”几乎等同自然语言。(docs.cangjie-lang.cn)

main() {
    var sum = 0
    for (i in 1..=100) { sum += i }
    println(sum)  // 5050
}

5.2 元组解构:一次性拆出多值

当集合元素为元组时,可在循环头部直接解构,避免在循环体里反复下标访问。(docs.cangjie-lang.cn)

main() {
    let points = [(1, 2), (3, 4), (5, 6)]
    for ((x, y) in points) {
        println("${x}, ${y}")
    }
}

5.3 where 条件:进入循环体前就过滤

当只对“满足条件的元素”做处理,where 比先 ifcontinue 更清爽,且编译器能更好地做局部分析与告警控制(未使用变量等)。(docs.cangjie-lang.cn)

main() {
    for (i in 0..8 where i % 2 == 1) {
        println(i)  // 1 3 5 7
    }
}

工程思考

  • 迭代变量不可修改是语言规则,有助于减少循环内部的副作用(见官方文档对应说明)。如需索引变换,请引入新变量而非直接赋值。(docs.cangjie-lang.cn)
  • 序列只求值一次意味着:若“序列”表达式昂贵,放在 for 头部也安全;若需要动态生成序列,则考虑迭代器/生成器式设计。

6. 实战 C:while-let 驱动的拉取式处理(I/O、解析、异步队列)

目标:展示如何把“尝试取一个元素,如果有就处理并继续,否则停止”的惯用模式写得既简洁又安全。

6.1 while-let 的语义回顾

while (let PATTERN <- EXPR) { ... }:对 EXPR 求值,与 PATTERN 进行匹配;若成功,进入循环体;失败则退出。常用于 Option<T>、枚举、迭代器的“下一项”。这在官方指南中被系统性介绍,尤其适合“接收数据直到失败/结束”的情景。(docs.cangjie-lang.cn)

6.2 案例:Option 拉取与解析

假设我们有一个可能返回 Some(T)Nonenext() 风格函数(如网络接收、流式读取、解析器窥探等),while-let 能将读取 + 判空 + 解构折叠为一行。

// 模拟:从数据源尝试拉取一字节,成功返回 Some(b),失败返回 None
import std.random.*

func recv(): Option<UInt8> {
    let n = Random().nextUInt8()
    if (n < 200) { return Some(n) }
    return None
}

main() {
    // 只要还能收到数据,就持续处理;失败则退出
    while (let Some(data) <- recv()) {
        // 在此做校验/聚合/统计/写入等处理
        if (data % 16 == 0) { println("block boundary: ${data}") }
    }
    println("receive finished.")
}

这个模式与官方示例精神一致:条件中直接匹配,让控制流变得“声明式”,同时避免漏判 None。(docs.cangjie-lang.cn)

工程思考

  • 背压与节流:若数据来得太快、处理跟不上,循环内应在合适位置引入批量提交节流策略(例如计数到 N 再 flush)。
  • 错误与重试:把“失败”与“致命错误”区分开:None 代表“结束/暂不可用”,而真正的 I/O 错误应通过 Result<T, E>(或抛异常)显式上抛。
  • 可测试性:将 recv() 作为依赖注入(传入函数指针/接口实现),让循环逻辑可单测(注入固定序列,验证聚合结果)。

7. 可维护性的 10 条实践建议(含性能与安全)

  1. 优先 for-in:遍历集合/区间时,for-in 的语义密度更高;用 where 对进入循环体的元素“预筛”。(docs.cangjie-lang.cn)
  2. 把“停止条件”写清楚while 的条件要体现收敛,比如“误差平方 < 阈值”“计数达到上限”;避免在循环体内“隐式 break”。(docs.cangjie-lang.cn)
  3. 首轮即有副作用 → do-while:当业务语义是“先做一次再说”,用 do-while 封装第一步。(docs.cangjie-lang.cn)
  4. 可空/可失败数据 → while-let:与 Option/枚举天然匹配,降噪且避免漏判。(docs.cangjie-lang.cn)
  5. 避免修改迭代变量:仓颉明确禁止,这有助于减少副作用;用新变量承载变换后的值。(docs.cangjie-lang.cn)
  6. 序列只求值一次:在 for-in 里把昂贵表达式放到“序列”位置是安全的,但也因此要留意共享状态是否被多次迭代使用。(docs.cangjie-lang.cn)
  7. 把“过滤”提前:能用 where 就别把大量 if { continue } 放在循环体首行。(docs.cangjie-lang.cn)
  8. 控制复杂度:循环体一旦超过 20 行,考虑提炼为函数;通过早返回守卫式条件降低嵌套。
  9. 幂等与副作用边界:I/O 写入、网络调用等副作用应集中在少数点位,便于重试与容错。
  10. 可观测性:长循环要有进度日志/指标;数值迭代要在达不到收敛时暴露保护性上限并告警。

8. 常见坑与规避

  • 把非布尔作为条件:仓颉的条件必须是布尔;不能像 C 一样用 0/非0 代替,否则编译报错。(docs.cangjie-lang.cn)
  • for-in 内修改迭代变量:属于语义违背;应使用新的可变变量存放变换结果。(docs.cangjie-lang.cn)
  • 把“过滤”放在循环体里:不仅冗长,还可能引入未使用变量的告警;_where 是更好的表达手段。(docs.cangjie-lang.cn)
  • 遗漏 None/失败分支:拉取式处理应使用 while-let,天然避免漏判与空指针式错误。(docs.cangjie-lang.cn)

9. 进阶话题(给想“抠细节”的同学)

  • 与模式匹配的融合while-let 左侧支持多种模式(如常量、通配符、绑定、元组、枚举),可以构建“只对某几种枚举分支持续处理”的精细逻辑。(humid1ch.cn)
  • 与类型推导相处:由于循环表达式本身是 Unit,把循环纳入更大表达式时不用担心“混型”;但若循环体最后一个表达式需要求值并参与推导,应将其提取到循环外。(docs.cangjie-lang.cn)
  • 可迭代协议与自定义容器for-in 依赖 Iterable<T>;当你实现自己的容器/生成器时,按协议暴露迭代器,即可与 for-in 无缝协作(数组、区间已内置实现)。(docs.cangjie-lang.cn)

10. 小结

在仓颉里,“循环是一等公民的表达式”。这看似一个“术语偏好”,其实在工程层面落地为三件事:

  1. 意图可见for-in 表达集合遍历,where 表达预过滤;while 把收敛写进条件;do-while 贴合“先做一次”;while-let 把可选/枚举处理写进循环头。
  2. 类型友好:循环作为表达式,与类型系统自然融合;配合 Option/枚举/模式匹配,减少空值与分支遗漏。
  3. 可维护:规则清晰、语法增益(解构、占位符、where)与明确限制(不可修改迭代变量)共同引导写出更稳、更干净的控制流。

当你下一次需要“处理一批数据、迭代逼近某个数值、从流里持续拉取直到结束”时,先想想“用哪个循环让意图最清楚”,再让仓颉的类型与语法为你兜底吧。🚀

参考与延伸阅读

  • 仓颉开发指南《表达式》:统一把 for-in/while/do-while/while-let 作为循环表达式(类型为 Unit),并给出 for-in 的区间遍历、解构、where、迭代变量不可修改等要点与示例。(docs.cangjie-lang.cn)
  • while-let:将取值与模式匹配折叠到循环条件中的官方说明与示例。(docs.cangjie-lang.cn)

如果你有具体业务场景(比如:日志清洗、协议解析、批处理 ETL、图算法遍历等),我也可以据此把上面的模式“落地成一段可直接跑的模块化代码”,顺便加上性能对比与基准测量建议。要不要来一发?🙂

如果觉得有帮助,别忘了点个赞+关注支持一下~
喜欢记得关注,别让好内容被埋没~

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐