Kotlin 07:标签规则里的闭包与返回
有两条任务标签 "Queued"、"Urgent",先统一为小写,再筛选指定前缀。如果前缀在筛选规则创建后、真正调用前发生变化,筛选依据是旧值还是新值?把规则写成 lambda 后,答案由捕获了什么以及何时调用决定,不取决于 lambda 看起来是否“像一段配置”。
沿用 06 的标签查询,本篇只让转换和过滤规则变成可传入的函数;仍是同一进程中的同步集合变换,没有线程池、后台队列或协程。源码位于 examples/kotlin-language/src/main/kotlin/example/tasks/TaskLabelRules.kt,环境继续固定为 Kotlin/JVM 2.2.21、JDK 21 和 Gradle Wrapper 8.14。
函数类型写清了谁负责转换、谁负责决定保留
这里不需要为两条规则先建抽象工厂。传入两个函数就够了:
1 | |
按照Kotlin 函数类型文档,(String) -> String 表示接受一个字符串并返回字符串;(String) -> Boolean 是判断函数。map 把每项交给 normalize,再用 filter 的 accept 决定保留哪些结果。normalize 与 accept 现在是同步调用的函数值,不是后台任务或事件订阅;返回的 List<String> 也不表示查询已落库。
实验在创建两个 lambda 后把可变局部变量 prefix 从 "u" 改成 "q",然后才执行筛选:
1 | |
第一次调用 selectTaskLabels(listOf("Queued", "Urgent"), normalize, accept) 返回 listOf("queued");第二次把 prefix 改回 "u",同一个 lambda 返回 listOf("urgent")。官方闭包小节允许 lambda 访问并修改外部变量。在本例中,prefix 在 lambda 执行时读取的是当时的局部变量值,并未在 lambda 被创建时冻结为一份副本;这里没有跨线程访问,不能把这个实测直接写成并发可见性承诺。
测试还给两个函数各加了计数器:一次调用中两条标签各被 normalize 和 accept 访问一次,故计数都为 2;第二次调用后都为 4。这是当前 List.map(...).filter(...) 与两个输入的运行观察,不是“所有 lambda 都会立即运行”的定理。08 将对比 Sequence:在那里,中间操作何时求值必须另测,不能沿用此处计数。
return@forEach 跳过一次,return 可以退出外层函数
过滤空白标签时,return@forEach 只结束当前这次 lambda 调用:
1 | |
输入 listOf("queued", " ", "urgent") 得到 listOf("queued", "urgent"),说明空白项被跳过,但后面的 "urgent" 仍处理。官方标记返回文档规定了 return@forEach 的目标;这不是对整个 nonBlankTaskLabels 的提前返回。
找首个匹配标签时,目标反而是直接结束外层函数:
1 | |
测试依次传入 "queued"、"urgent"、"later",匹配第二项,记录的访问列表只有前两项。forEach 在此处是 inline 调用,直接传入的 lambda 可以执行这种非局部返回;依据是官方 inline 函数的 non-local jump 小节,不是函数类型 (String) -> Boolean 自身提供了魔法。
为界定条件,verification/07-nonlocal-return.kt 把同样的裸 return 放进非 inline 的 invokeCallback { ... }。此文件独立于默认构建编译,退出 1,冻结编译器实际诊断 'return' is prohibited here.。不能把非法样本放进正常源码集,也不能把 return@forEach 改写成“在任何回调里都能退出外层函数”。上述函数没有取消语义;将来若回调变成异步执行,返回边界和调用时机需要重新定义。
哪些证据可以重现
在仓库根目录运行 cd examples/kotlin-language && bash verification/07.sh。脚本先记录 UTC 环境、JDK 与 Gradle 信息,离线 clean test 退出 0;00–07 八组测试各 2/2。新增 TaskLabelRulesTest 分别断言捕获值的两次读取、每次 map/filter 的调用次数、带标签返回与非局部返回;隔离编译拒绝退出 1。旧 CLI 的 submit alpha 为 0,缺文本为 1,只是回归证明,标签规则尚未接入 CLI。真实命令、XML、诊断和逐项断言在 writing-plans/kotlin/verification/07/。
练习与答案
练习一:先创建读取 prefix 的 lambda,再把 prefix 从 "u" 改成 "q",随后处理 "Queued",会保留吗?答:会。函数先转成 "queued",调用 accept 时读取到的是改后的 "q";不能从 lambda 创建时间推断捕获值已经冻结。
练习二:输入 "queued"、空白、"urgent",return@forEach 会不会让整个函数只返回 "queued"?答:不会,跳过的是空白那次调用,循环仍处理 "urgent";测试断言两条非空标签均保留。
练习三:为什么 firstMatchingTaskLabel 的 return label 可编译,放入普通 invokeCallback 的 return 却被拒绝?答:前者的 lambda 直接传给 inline forEach,可以非局部返回;后者回调由非 inline 函数调用,无法在调用现场完成同样的控制流。具体的拒绝诊断应以当前编译器记录为准。
下一篇再比较同一变换使用急切集合与 Sequence 时,哪些函数会在取前几个结果前真正调用。






