契约 API (Contracts)
Kotlin 编译器的智能转换 (Smart Cast) 和控制流分析 (Control Flow Analysis) 功能非常强大,但它通常局限于当前函数体内部。对于跨函数的调用,编译器往往采取保守策略,无法感知被调用函数的内部逻辑(如非空校验、Lambda 调用次数等)。Kotlin Contracts (契约) 机制允许库开发者向编译器明确声明函数的行为,从而让编译器在调用处进行更精确的静态分析。
实验性特性
契约 API 目前仍处于 Experimental 阶段。使用时需要添加 @OptIn(ExperimentalContracts::class) 注解。尽管如此,Kotlin 标准库 (stdlib) 已经广泛使用了契约(如 run, apply, require, synchronized 等),其行为定义是稳定的。
核心痛点:编译器视角的局限
在没有契约的情况下,编译器无法透过函数签名看到内部实现,导致很多符合逻辑的代码无法通过编译。
fun String?.isNotNull(): Boolean {
return this != null
}
fun process(text: String?) {
if (text.isNotNull()) {
// ❌ 编译错误:Only safe (?.) or non-null asserted (!!.) calls are allowed
// 编译器不知道 isNotNull() 返回 true 意味着 text != null
println(text.length)
}
}fun executeExactlyOnce(block: () -> Unit) {
block()
}
fun initCheck() {
val x: Int
executeExactlyOnce {
x = 10
}
// ❌ 编译错误:Variable 'x' must be initialized
// 编译器无法保证 block 一定被执行,也无法保证仅执行一次
println(x)
}契约 DSL 详解
契约通过 contract 构建器定义,它是 DSL 风格的 API。目前主要支持两类契约:Returns Contracts (返回值与参数状态的关联) 和 CallsInPlace Contracts (Lambda 的调用行为)。
Returns Contracts:强化智能转换
returns 契约用于告知编译器:"如果函数返回特定值,则意味着参数满足特定条件"。这使得调用侧可以直接享受智能转换的红利。
API 核心签名
// kotlin.contracts.ContractBuilder
fun returns(): Returns
fun returns(value: Any?): Returns
fun Returns.implies(booleanExpression: Boolean): ConditionalEffect实战:自定义前置检查
标准库中的 require 和 check 就是典型的例子。我们可以模仿实现一个自定义的断言函数。
import kotlin.contracts.*
@OptIn(ExperimentalContracts::class)
fun requireString(value: Any?) {
contract {
// 如果函数正常返回(没有抛出异常),则意味着 value 是 String 类型
returns() implies (value is String)
}
if (value !is String) throw IllegalArgumentException("Not a string")
}
fun testSmartCast(input: Any?) {
requireString(input)
// ✅ 智能转换生效:编译器现在知道 input 是 String
println(input.length)
}实战:多条件蕴含
契约不仅支持 null 检查,还支持类型检查 (is) 和布尔逻辑。
@OptIn(ExperimentalContracts::class)
fun CharSequence?.isNotNullOrEmpty(): Boolean {
contract {
// 返回 true 意味着 this 既不为 null 也不为空
returns(true) implies (this@isNotNullOrEmpty != null)
}
return this != null && this.isNotEmpty()
}@OptIn(ExperimentalContracts::class)
fun Any?.isStringAndValid(): Boolean {
contract {
// 返回 true 意味着 this 是 String
returns(true) implies (this@isStringAndValid is String)
}
return this is String && this.length > 5
}CallsInPlace Contracts:完善初始化分析
callsInPlace 契约用于告知编译器传递给函数的 Lambda (Functional Interface) 将在何处、如何被执行。这对于变量的一次性初始化 (Val Reassignment) 和控制流完整性至关重要。
API 核心签名
// kotlin.contracts.ContractBuilder
fun <R> callsInPlace(lambda: Function<R>, kind: InvocationKind = InvocationKind.UNKNOWN): CallsInPlaceInvocationKind 枚举详解
| 类型 | 含义 | 典型场景 |
|---|---|---|
EXACTLY_ONCE | 保证 Lambda 被执行且仅执行一次 | run, let, apply, synchronized |
AT_LEAST_ONCE | 保证 Lambda 至少被执行一次 | retry, 某些循环结构 |
AT_MOST_ONCE | Lambda 可能不执行,但绝不会执行多次 | if, Optional.ifPresent |
UNKNOWN | 默认值,不做任何保证 | 异步回调、存储起来稍后调用的 Lambda |
实战:自定义作用域函数
通过 callsInPlace,我们可以编写类似 run 的辅助函数,支持在 Lambda 内部初始化外部的 val 变量。
import kotlin.contracts.*
@OptIn(ExperimentalContracts::class)
inline fun <T, R> T.mapAndLog(transform: (T) -> R): R {
contract {
// 承诺 transform 会在当前函数内被调用一次
callsInPlace(transform, InvocationKind.EXACTLY_ONCE)
}
println("Transforming...")
return transform(this)
}
fun testInitialization() {
val result: String
100.mapAndLog {
// ✅ 允许初始化 val,因为编译器确信此 Lambda 只执行一次
result = "Value is $it"
it.toString()
}
println(result) // ✅ 编译器确信 result 已被初始化
}编译器信任与风险
契约机制的核心建立在"编译器信任开发者"的基础上。编译器不会去验证你写的 contract 代码块是否真实反映了函数的实际行为。
契约欺诈 (Lying to the Compiler)
如果你定义的契约与实际代码逻辑不符,会导致严重的运行时错误(如 ClassCastException 或 NullPointerException),因为编译器基于错误的假设优化了代码。
@OptIn(ExperimentalContracts::class)
fun isString(input: Any?): Boolean {
contract {
// ⚠️ 谎言:返回 true 意味着 input 是 String
returns(true) implies (input is String)
}
// 实际逻辑:只判断了不为 null
return input != null
}
fun crashMe(obj: Any?) {
if (isString(obj)) {
// 💥 运行时崩溃:ClassCastException
// 编译器移除了 obj as String 的检查,直接作为 String 使用,但 obj 可能是 Int
println((obj as String).length)
}
}技术限制与最佳实践
使用限制
- 位置限制:
contract调用必须是函数体的第一条语句。 - 顶层函数:目前契约主要支持顶层函数(Top-level functions)和类的成员函数。
- 无副作用:
contract块内的代码纯粹是为了生成元数据,不会在运行时执行。你不能在contract块中使用变量、打印日志或执行任何逻辑运算。
最佳实践
- 优先用于基础库:契约主要适用于编写通用的工具函数、断言库或 DSL 基础结构。业务逻辑层通常不需要定义契约。
- 配合 Inline 使用:对于
callsInPlace,通常配合inline函数使用效果最佳,因为非内联函数的 Lambda 调用对于编译器来说是黑盒,即使有契约,部分控制流分析也可能受限。 - 保持简单:契约逻辑应尽可能直观,避免复杂的布尔表达式,减少出错概率。
底层原理:元数据传播
当编译器编译带有 contract 的函数时,它不会将 contract 代码块编译成 Java 字节码指令。相反,它将契约信息序列化并存储在生成的 .class 文件的元数据 (Metadata) 中。 当其他代码调用该函数时,编译器读取该元数据,将其加载到控制流分析 (CFA) 引擎中,从而扩展了其对变量状态和执行路径的推断能力。这也是为什么契约必须位于函数第一行的原因——它更像是函数签名的一部分,而非执行逻辑。
总结
Kotlin Contracts 填补了"开发者意图"与"编译器推断"之间的鸿沟。
- 使用
returns(...) implies (...)消除冗余的类型检查和空安全调用。 - 使用
callsInPlace(...)支持在自定义高阶函数中安全地初始化变量。 - 谨记:能力越强,责任越大。确保契约的准确性是开发者的责任,不要欺骗编译器。