之前翻 Kotlin 标准库源码的时候,发现很多顶层函数的开头都有一段 contract { ... } 块。第一次看的时候完全不知道在干什么,后来才搞明白这是 Kotlin 1.3 引入的契约(Contract) 机制------本质上是给编译器提供额外的类型推断信息。
简单来说:契约不影响运行时行为,它只在编译阶段"告诉"编译器一些事情。最直觉的例子就是智能转换(smart cast),你在用 is 判断类型之后,编译器自动把变量当成目标类型来用,这背后就是 contract 在起作用。
基本语法
contract 声明必须放在函数体的第一行,且至少包含一个效果(effect)。Kotlin 1.3 引入时仅支持顶层函数,1.7 起已解除这个限制,成员函数和局部函数也可以用 contract 了。
注意:如果你用的是 Kotlin 1.x,函数签名上需要加
@ExperimentalContracts注解;Kotlin 2.x 起这个注解可能已经不需要了,看编译器提示即可。
less
/**
* 指定函数的契约。
* 契约描述必须放在函数体的开头,且至少有一个效果。
*/
@ContractsDsl
@InlineOnly
@SinceKotlin("1.3")
@Suppress("UNUSED_PARAMETER")
public inline fun contract(builder: ContractBuilder.() -> Unit) { }
这个函数本身是空的------它只在编译期生效,运行时什么也不做。
约定暗示(implies)
returns + implies 组合是最常用的契约形式,用来表达"当函数返回某个值时,某个条件一定成立"。
另外 returns() 无参形式也挺实用,表示"函数正常返回(不抛异常)时条件成立",在 guard 类函数中经常用到:
kotlin
// returns() 无参形式:函数正常返回时,value 一定非空
@ExperimentalContracts
fun requireValue(value: String?) {
contract {
returns() implies (value != null)
}
require(value != null)
}
fun demo(value: String?) {
requireValue(value)
println(value.length) // 智能转换为 String,安全访问
}
最常用的还是带参形式:
kotlin
private fun isBird(animal: Animal): Boolean {
contract {
// 如果 isBird 返回 true,则 animal 一定是 Bird 类型
returns(true) implies (animal is Bird)
}
return animal is Bird
}
// 使用处:不需要手动 as 转换,编译器已经知道了
fun printBirdName(animal: Animal) {
if (isBird(animal)) {
println(animal.name) // 智能转换为 Bird,可以安全访问 name
}
}
这个例子展示了 contract 最核心的价值:把函数内部的类型判断逻辑"暴露"给编译器,让它在调用处也能做出正确的类型推断。
闭包执行次数约定(callsInPlace)
callsInPlace 用来告诉编译器一个 lambda 参数会被调用几次。这在变量初始化场景下特别有用。
InvocationKind 枚举值
callsInPlace 的第二个参数接受 InvocationKind 枚举:
| 枚举值 | 含义 |
|---|---|
EXACTLY_ONCE |
lambda 一定会被调用且仅调用一次 |
AT_MOST_ONCE |
lambda 最多调用一次(可能不调用) |
AT_LEAST_ONCE |
lambda 至少调用一次(可能多次) |
UNKNOWN |
调用次数不确定 |
EXACTLY_ONCE 最常用,它直接解决了"lambda 内部初始化变量"的编译器报错问题。
为什么需要这个约定
先看一个不加 contract 会报错的例子:
kotlin
fun initStr(block: () -> Unit) {
// 没有 contract,编译器不知道 block 会不会执行
block()
}
fun demo() {
var str: String
initStr {
str = "hello"
}
println(str.length) // 编译错误:str 可能未初始化
}
编译器无法确定 block 是否一定被执行,所以认为 str 可能没有赋值。
加上 contract 之后:
kotlin
fun initStr(block: () -> Unit) {
contract {
// 告诉编译器:block 一定会被调用一次
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block()
}
fun demo() {
var str: String
initStr {
str = "hello"
}
println(str.length) // OK,编译器知道 str 一定被初始化了
}
实际应用:DSL 构建器
这个模式在 DSL 构建中很常见。比如写一个配置构建器:
kotlin
class ServerConfig {
var host: String = ""
var port: Int = 0
fun build(block: ServerConfig.() -> Unit): ServerConfig {
contract {
// block 一定会执行,所以 block 内的赋值是可靠的
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block()
return this
}
}
// 使用处
fun demo() {
val config = ServerConfig().build {
host = "localhost"
port = 8080
}
println("${config.host}:${config.port}")
}
核心思路一样:通过 EXACTLY_ONCE 约定,编译器确认 lambda 内的赋值一定会发生。
返回值暗示非空
contract 还能用来做非空推断。当一个可空类型的扩展函数通过 returns(true) implies 约定返回 true 时一定非空,后续代码就不需要 ?. 或 !! 了。
kotlin
fun String?.isNotNullOrEmpty(): Boolean {
contract {
// 如果返回 true,则 this 一定不为 null
returns(true) implies (this@isNotNullOrEmpty != null)
}
return this != null && !this.trim().equals("null", true) && this.trim().isNotEmpty()
}
// 使用处
fun handleInput(input: String?) {
if (input.isNotNullOrEmpty()) {
// 编译器已经把 input 当作 String(非空)处理
println(input.length)
}
}
contract vs require / check
我一开始也分不清 contract 和 require / check,后来才搞明白它们完全不在一个维度上。
contract(契约) 是编译期的提示机制,不产生任何运行时代码。它的作用是告诉编译器"相信我,这个条件一定成立",让编译器做出更精准的类型推断和空安全分析。
require / check 是运行时的前置条件检查,条件不满足时直接抛异常。require 抛 IllegalArgumentException,check 抛 IllegalStateException。
kotlin
// require:运行时校验,条件不满足直接崩溃
fun setColor(color: String) {
require(color.startsWith("#")) { "颜色值必须以 # 开头" }
// ...
}
// contract:编译期提示,不会产生运行时开销
fun isValidColor(color: String): Boolean {
contract {
returns(true) implies (color.startsWith("#"))
}
return color.startsWith("#") && color.length == 7
}
简单记:require / check 是用来拦截非法输入 的,contract 是用来辅助类型推断的。两者可以配合使用,但不能互相替代。