taskProposal("alpha") 没有传重试次数,究竟用了什么值?如果改成 taskProposal(" ", retries = 6),调用方得到 null,还是抛出异常?参数列表和返回类型把这些问题分开了:省略实参有缺省规则,空白内容可以没有预览结果,非法的重试配置则必须被拒绝。

沿用 02 的命令解析器,本篇给同一个合成任务系统增加一个尚未接入 CLI 的输入预览函数。它只返回格式化文本,没有任务实体、队列、执行器或持久化;函数名中的 proposal 也不表示已经提交。实验环境仍是 Kotlin 2.2.21、Gradle 8.14、JDK 21。

省略参数与显式覆盖是不同的调用

核心函数的公开契约很短:

1
2
3
4
5
6
7
fun taskProposal(text: String, retries: Int = 2, vararg labels: String): String? {
if (retries !in 0..5) rejectRetries(retries)
require(labels.all(String::isNotBlank)) { "Labels must not be blank" }
if (text.isBlank()) return null
val suffix = if (labels.isEmpty()) "none" else labels.joinToString(",")
return "TASK proposal (not submitted): $text; retries=$retries; labels=$suffix"
}

缺省值 2 只在没有为 retries 提供实参时生效;传 0 不会因为它看起来“像没填”而自动改成 2。测试分别调用 taskProposal("alpha"),得到 retries=2; labels=none;调用 taskProposal("beta", 0, "urgent"),得到 retries=0; labels=urgent。该函数目前没有把标签保存进任何任务对象,测试只检查返回文本。

命名调用允许把参数放在声明顺序之外:

1
2
taskProposal(labels = arrayOf("one", "two"), text = "alpha", retries = 4)
taskProposal("beta", 0, *arrayOf("urgent"))

第一行的 labels 是命名实参,可以直接传数组;第二行是位置实参,要把已有数组展开为 vararg 的元素。Kotlin 2.2.21 对命名数组再加 * 给出“多余的展开运算符”警告,所以测试保留两种实际可验证的写法。官方函数文档:默认参数、命名实参、可变数量参数分别定义这些调用规则;这里的 vararg 在函数体内作为数组使用,不等于业务上的“批量提交”。

null、异常和 Unit 各回答什么

String? 表示可能没有预览文本:空白任务文本返回 null,而不是一个已建立的任务。retries 限于 0..5,标签不得空白;检查发生在空文本返回之前,因此 taskProposal(" ", retries = 6) 也抛出异常,不会悄悄吞掉非法配置。测试分别断言空白返回、非法 6、空标签与同时为空白且非法配置的路径。

拒绝重试值的函数声明为 Nothing:

1
2
fun rejectRetries(retries: Int): Nothing =
throw IllegalArgumentException("Retries out of range: $retries")

Nothing 是不正常返回的底类型;本例的运行结果是抛出 IllegalArgumentException,不是返回一个隐藏的 null。其编译器类型规则见官方异常文档的 Nothing 小节。taskProposal 本身会正常返回 String?;只有越界分支调用拒绝函数。

与之相对,emitProposal(proposal, emit) 的返回类型为 Unit,只把已形成的预览文字交给调用者提供的回调;传 null 时交给回调的是 No proposal: blank task text。测试捕获一个本地列表,检查一次回调和 Unit 结果。官方函数文档的 Unit 小节说明它是只有一个值的类型;这里的回调没有写入持久层,也没有启动后台任务。

把三个接口连起来看:String? 让调用者处理没有预览的情况;Nothing 清楚地标记不可继续的失败分支;Unit 不提供一个可用于声称“任务创建成功”的结果。它们表达的不是同一种“没有值”。

Java 方法不能照搬 Kotlin 命名实参

Kotlin 代码调用自己的 taskProposal 时可以写 retries = 4。调用 java.lang.Math.max(a = 1, b = 2) 则不行:官方命名实参文档的 Java 注记明确限制 Kotlin 对 Java 方法的命名调用,Java 字节码不总是保留可供此语法依赖的参数名。

verification/03-java-named-args.kt 被隔离在正常源码集外;在冻结版本下单独编译退出码 1,编译器列出 max(p0: Int, p1: Int) 等不匹配的候选方法。这是该版本的实际诊断,不是所有编译器都必须逐字复现的语言条文。也不应从这个拒绝样本倒推出 Java 调用 Kotlin 的全部规则;19–20 才处理双向互操作。

复跑与证据边界

在 examples/kotlin-language/ 执行 bash verification/03.sh。脚本离线 clean test,保留 00–02 的三个 suite,新增 TaskProposalTest 的两项断言;原 CLI 的 submit alpha 退出 0,缺文本的 submit 退出 1,隔离 Java 命名调用编译也预期退出 1。原始命令、环境、XML、诊断和断言留在 writing-plans/kotlin/verification/03/。这些结果证明当前工具链下的参数/返回边界,不证明任务已经执行、网络或数据库可用。

下载含Wrapper、完整累计工程与证据的03快照(SHA-256校验)。如果云端没有 `examples/`,按同名素材目录的 `README.md` 解包并复跑;不要把来源页面、实际测试和 Hexo 渲染误记为同一次验证。

练习与答案

练习一:taskProposal("alpha", retries = 0) 会不会使用缺省重试次数 2?答:不会。0 是显式实参,且在允许范围内;返回结果中的重试次数是 0。

练习二:传入 text = " "、retries = 6 时先发生什么?答:先检查重试次数并抛异常;空白返回 null 的分支在检查之后。把两行代码交换位置会改变这个契约,因此测试明确覆盖组合输入。

练习三:Unit 的回调运行后就能说预览已持久化吗?答:不能。测试只看到本地列表收到字符串;真正提交必须有独立的写入操作及失败契约,不能从回调的返回类型反推。

下一篇检查 Unicode 字符串的索引、字符数和编码字节数;本篇返回的 String 还没有给“长度”作出任何字节级保证。