Skip to content

契约 API (Contracts)

源:Kotlin Contracts

Kotlin 编译器的智能转换 (Smart Cast) 和控制流分析 (Control Flow Analysis) 功能非常强大,但它通常局限于当前函数体内部。对于跨函数的调用,编译器往往采取保守策略,无法感知被调用函数的内部逻辑(如非空校验、Lambda 调用次数等)。Kotlin Contracts (契约) 机制允许库开发者向编译器明确声明函数的行为,从而让编译器在调用处进行更精确的静态分析。

实验性特性

契约 API 目前仍处于 Experimental 阶段。使用时需要添加 @OptIn(ExperimentalContracts::class) 注解。尽管如此,Kotlin 标准库 (stdlib) 已经广泛使用了契约(如 run, apply, require, synchronized 等),其行为定义是稳定的。

核心痛点:编译器视角的局限

在没有契约的情况下,编译器无法透过函数签名看到内部实现,导致很多符合逻辑的代码无法通过编译。

kotlin
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)
    }
}
kotlin
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
// kotlin.contracts.ContractBuilder
fun returns(): Returns
fun returns(value: Any?): Returns
fun Returns.implies(booleanExpression: Boolean): ConditionalEffect

实战:自定义前置检查

标准库中的 requirecheck 就是典型的例子。我们可以模仿实现一个自定义的断言函数。

kotlin
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) 和布尔逻辑。

kotlin
@OptIn(ExperimentalContracts::class)
fun CharSequence?.isNotNullOrEmpty(): Boolean {
    contract {
        // 返回 true 意味着 this 既不为 null 也不为空
        returns(true) implies (this@isNotNullOrEmpty != null)
    }
    return this != null && this.isNotEmpty()
}
kotlin
@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
// kotlin.contracts.ContractBuilder
fun <R> callsInPlace(lambda: Function<R>, kind: InvocationKind = InvocationKind.UNKNOWN): CallsInPlace

InvocationKind 枚举详解

类型含义典型场景
EXACTLY_ONCE保证 Lambda 被执行且仅执行一次run, let, apply, synchronized
AT_LEAST_ONCE保证 Lambda 至少被执行一次retry, 某些循环结构
AT_MOST_ONCELambda 可能不执行,但绝不会执行多次if, Optional.ifPresent
UNKNOWN默认值,不做任何保证异步回调、存储起来稍后调用的 Lambda

实战:自定义作用域函数

通过 callsInPlace,我们可以编写类似 run 的辅助函数,支持在 Lambda 内部初始化外部的 val 变量。

kotlin
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)

如果你定义的契约与实际代码逻辑不符,会导致严重的运行时错误(如 ClassCastExceptionNullPointerException),因为编译器基于错误的假设优化了代码。

kotlin
@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(...) 支持在自定义高阶函数中安全地初始化变量。
  • 谨记:能力越强,责任越大。确保契约的准确性是开发者的责任,不要欺骗编译器。