Kotlin - 约定contract

之前翻 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 是运行时的前置条件检查,条件不满足时直接抛异常。requireIllegalArgumentExceptioncheckIllegalStateException

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 是用来辅助类型推断的。两者可以配合使用,但不能互相替代。

相关推荐
Junerver2 小时前
使用datetime更加优雅地在kotlin中处理时间
kotlin
装杯让你飞起来啊4 小时前
第 4 周 Unit 2:Jetpack Compose 状态、按钮、计数器与小费计算器
windows·microsoft·kotlin·安卓
Kapaseker9 小时前
MVVM 旧城改造,边界划分各有招
android·kotlin
装杯让你飞起来啊1 天前
第 2 周 Day 5-6:综合小游戏 —— 学生成绩管理系统
windows·microsoft·kotlin
装杯让你飞起来啊1 天前
Kotlin List / Array 与 for 循环
开发语言·kotlin·list
装杯让你飞起来啊1 天前
混合练习 —— 猜数字游戏
windows·游戏·kotlin
装杯让你飞起来啊1 天前
Kotlin 条件判断 if / when 与智能转换 smart cast
开发语言·python·kotlin
pengyu1 天前
【Kotlin 协程修仙录 · 金丹境 · 初阶】 | 并发艺术:async/await 与并发组合的优雅之道
android·kotlin
黄林晴1 天前
重磅发布!KMP 双端订阅支付彻底封神,一套代码搞定 iOS+Android
android·kotlin