前言:
你是否曾将 if (obj != null)提取成一个独立函数后,Kotlin 的智能转换(Smart Cast)就神奇地"失效"了?这不是编译器的bug,而是它的能力边界。Kotlin Contracts 正是打破这道边界的神奇钥匙。本文将基于 Kotlin 最新版本 v2.2.21 全面讲解 Kotlin Contracts 的所有特性及其使用方式。
本文基于Android Studio Otter | 2025.2.1 & Kotlin v2.2.21
What?
Contracts(契约) 是一种编译时机制,是一种让编译器更了解函数行为的特性,它允许函数作者明确地告诉编译器一些关于函数行为的约束,从而帮助编译器进行更准确的类型推断和智能转换。
嗯...... 不懂, 说人话!
首先我们先从它的效果来直观了解一下吧
先看两个示例:
kotlin
fun foo(s: String?) {
// 编译器自动将's'转换为了'String' 而不是'String?'
if (s != null) s.length
}
fun onClick(v:View?) {
if (v is TextView) {
// 编译器自动将 'v' 转换为了'View',因此可以直接调用TextView 的方法
v.setText("TextView")
}
}
以上两个示例表明 Kotlin 编译器可以智能的根据上下文的类型检查条件来自动的进行类型转化,这是因为 Kotlin 编译器会进行大量的静态分析,这就是 Kotlin 编译器的智能转换。
What ? 这我知道呀,用你说? 这里面也没有 Contracts 呀?
别急,上面的示例虽然可以利用 Kotlin 的自动的智能转换,但是一旦将这个功能提取到一个单独的函数中时,这些智能转换功能就消失了,且看:
kotlin
fun String?.isNotNull(): Boolean = this != null
fun foo(s: String?) {
if (s.isNotNull()) s.length // No smartcast :(
}
因此为了改善这种情况,Kotlin 在 1.3 版本 中引入了 Contracts 机制。
Why?
上面对于 Contracts 是什么的解释其实也同时回答了为什么会有 Contracts ,但看到这里有人可能还会有疑问,为什么单独拿到一个函数中,这种「智能转换」就失效了,原因其实很简单,因为这个「新函数」是你自己写的,Kotlin 编译器无法 100% 的确认你的 isNotNull 函数真的可以产生对应的效果,因此需要开发者通过一种途径也就是「Contracts」来明确这一点,并且在Kotlin官方文档中也明确表示 「编写正确合理的 Contracts 是程序员的责任」
总结:
Contracts 允许函数的作者(也就是阁下😎) 以编译器能够理解的方式 显示的 描述你所写的这个函数的行为。
说白了就是:你可以用 Contracts 这个东西来告诉 Kotlin 的编译器,你声明的函数是干什么用的。比如上面的 isNotNull 这个函数,你在没有使用 Contracts 之前,这个函数的作用只是为了给其他的开发者或者你自己看 的,用来告诉调用者这个函数如果返回 true 就说明传入的参数不为空,但是,编译器并不知道,所以使用 Contracts 来让 Kotlin 编译器也能看懂你这个函数的作用。
How?
以上只是介绍了 Contracts 支持的其中一种情况,Kotlin 中称为 Effect(效果),Kotlin 使用 Effect 接口来定义函数可能表现出的种种情况。截止到这篇文章发布时, Kotlin 的最新稳定版本是 Kotlin v2.2.21,在 Kotlin v2.2.20 中Contracts 新增了多项重大更新,在最新版的 Kotlin 中 Effect 目前定义了五种具体的 Effect ****(其在kotlin-stdlib标准库的kotlin.contract 包中定义),它们都是继承自 Effect 的接口
- SimpleEffect
- Returns
- ReturnsNotNull
- ConditionalEffect
- CallsInPlace
- HoldsIn
分别介绍一下:
SimpleEffect : Effect
这表示一种简单的效果,其中包含以下两种具体效果:
-
Returns : SimpleEffectReturns接口有两种实现(所有继承Effect 的接口都通过 在
ContractsBuilder接口中实现)-
returns(): Returns它表示函数正常返回(即不抛出异常),它通常与
implies结合使用,implies 表示「暗示」,意思就是告诉编译器 如果函数能正常返回则暗示着什么结果,函数调用处后面就会自动确认是这个结果应用场景:API 中的参数验证、构建器中的状态检查。在Kotlin标准库中,
require函数使用此效果进行前置条件强制。示例:
kotlin@OptIn(ExperimentalContracts::class) fun require(condition: Boolean) { contract { // 如果函数正常返回,则意味着函数调用者传入的条件是true returns() implies condition } //否则抛出异常 if (!condition) throw IllegalArgumentException() } fun example(x: String?) { require(x != null) // 通过Contract编译器知道x非空,因为require正常返回意味着x != null为true println(x.length) // 智能转换为非空 }kotlin//kotlin.contracts.Effect.kt 部分源码 @ContractsDsl @ExperimentalContracts @SinceKotlin("1.3") public interface SimpleEffect : Effect { @ContractsDsl @ExperimentalContracts //infix 表示这是个中缀函数 public infix fun implies(booleanExpression: Boolean): ConditionalEffect }PS:
returns() implies condition这种写法能够成立是因为 implies 是一个中缀函数,它等同于returns().implies(condition)可以明显的看出来,中缀函数的好处是能更形象的表达出中缀函数 前后两者的关系 -
returns(value: Any?): Returns
表示函数正常返回,并且返回值是 value, 并且在返回该值 暗示条件,它适用于谓词函数(过滤,条件判断等功能的函数,返回值是 Boolean)
应用场景:类型守卫、资格检查。增强复杂条件中的流类型分析。
示例:
kotlindata class User(val age: Int) @OptIn(ExperimentalContracts::class) fun isUser(user: Any?): Boolean { contract { returns(true) implies (user is User) } return user is User } fun greet(user: Any?) { if (isUser(user) && user.age > 18) { println("Hello, adult user:${user.age}") } }另外Strings.kt 源码中也有类似使用:
kotlin@kotlin.internal.InlineOnly public inline fun CharSequence?.isNullOrEmpty(): Boolean { contract { returns(false) implies (this@isNullOrEmpty != null) } return this == null || this.length == 0 } -
-
ReturnsNotNull : SimpleEffect它描述的是函数正常返回(不抛异常),并且返回值是非空的情况,官方文档说
returnsNotNull()是 v2.2.20 中新增的函数,但ReturnsNotNull这个Effect 在 Kotlin 1.3 中的源码中就已经有了,下面我们来分开说一下::-
Kotlin v2.2.20 以下版本中的
returnsNotNull():-
单独使用这个效果:
kotlin@OptIn(ExperimentalContracts::class) fun nonNullIfValid(input: String?): String { contract { returnsNotNull() // 表示函数总是返回非空(如果不抛异常) } if (input == null || input.isEmpty()) throw IllegalArgumentException() return input }看到这个代码,可能有人就发现了:
这不是脱裤子放屁吗,
nonNullIfValid的返回值已经声明是String了,表明它不会返回空值了呀确实,单独使用
returnsNotNull()确实没有实际的应用场景,因此它通常与 implies 来结合使用,如同returns()和returns(value)一样 -
returnsNotNull() impiles condition效果在左,条件在右,表示当函数返回非空值时 意味着 condition 这个条件是成立的。
应用场景:适用于"事后验证"(post-condition),从输出推断输入。适合谓词函数或处理器,当返回非空时,反推输入满足条件(如类型/非空)。常见于库 API 中,帮助调用者避免冗余检查。
示例:
kotlin@OptIn(ExperimentalContracts::class) fun processIfValid(input: Any?): String? { contract { // 如果返回非空,则 input 是非空 String returnsNotNull() implies (input != null && input is String) } if (input !is String) return null return input.trim() // 返回非空 String } fun useProcess(obj: Any?) { val result = processIfValid(obj) if (result != null) { // 编译器智能推断 obj 是非空 String(因为返回非空 ⇒ 条件真) println("useProcess:${obj.length}") // 无需 !! 或额外检查 } }
-
-
Kotlin 2.2.20 及以上版本中,returnsNotNull() 增加了新的使用方法及使用场景,也就是条件在左,效果在右:
(condition) implies returnsNotNull()含义:如果 condition 成立则函数返回非空值。
-
这是一种"正向推断":从已知条件推断返回行为。
-
编译器使用它来优化:当调用前条件已验证(如 通过 if 检查),则返回智能转换为非空类型,避免 ? 或 !!。
PS:与 v 2.2.20 之前的版本对比:
returnsNotNull() impiles condition:// 自 Kotlin 1.3,SimpleEffect 接口的方法(condition) implies returnsNotNull():// Kotlin 2.2.20 新增,Boolean 的扩展函数两者虽然都使用的是
implies这个中缀函数,但实际上这两个根本不是同一个函数(condition) implies returnsNotNull()中的 implies 是 v2.2.20 在 ContractBuilder 接口中新增的一个 infix 函数:kotlin/** * 指定当作为接收器参数传递的条件成立时,将观察到的效果。 * 目前仅支持ReturnsNotNull效果。 * Note: 接收器只能接受 boolean 类型的表达式,并且接收器或者叫函数参数会经历下面 * 两种处理: * 空检查 (`== null`, `!= null`); * 实例检查 (`is`, `!is`); */ @ExperimentalExtendedContracts @ContractsDsl public infix fun Boolean.implies(value: ReturnsNotNull)而
returnsNotNull() impiles condition中的 implies 则是 SimpleEffect 接口中的一个方法,这点将在下面的 ConditionalEffect 中详细讲解。应用场景:可空输入非空输出,适合处理可空输入的工具函数,当输入满足条件时,直接保证输出非空。替换了传统需要两个重载函数(可空版和非空版)的场景,提升代码简洁性。
示例:
kotlin@OptIn(ExperimentalContracts::class, ExperimentalExtendedContracts::class) fun decode(encoded: String?): String? { contract { //输入是非空时,保证输出非空 (encoded != null) implies (returnsNotNull()) } if (encoded == null) return null return java.net.URLDecoder.decode(encoded, "UTF-8") } fun useDecodedValue(s: String?) { //未判断输入非空时,函数需要使用'?'安全调用 decode(s)?.length if (s != null) { // 判断后编译器通过智能转换,返回值视为非空! println("decode length:${decode(s).length}") } }细心的人可能注意到 这个
decode方法比之前的示例多了一个 注解@OptIn(ExperimentalExtendedContracts::class)这是因为(condition) implies returnsNotNull()这种用法是 Kotlin2.2.20 新增的实验性的扩展功能并且如果要启用这个功能,需要在项目的
build.gradle(.kts)中额外的添加编译器选项:kotlinkotlin { compilerOptions { freeCompilerArgs.add("-Xallow-condition-implies-returns-contracts") } }⚠️ PS: 截至文章发布时,我使用的是 AndroidStudio 最新版:Android Studio Otter | 2025.2.1 ,官方文档提示:IntelliJ IDEA 目前仅在2025.3 EAP 版本 对新增的功能进行代码提示的更新,AndroidStudio 是跟随 IDEA 的,因此需要如果你当前也和我一样使用的是同一版本或者更低的 AndroidStudio ,那么你可能也会遇到以下问题: 即便已经标记了
@OptIn(ExperimentalExtendedContracts::class)并且也在build.gradle(.kts)中添加了上面的编译器选项,IDE 仍然会提示错误: ' implies 的这种用法不是 Contracts 的合法语句',不用担心,只要你配置正确,可以无视这个报错,直接编译运行不会出错,等到后期 AS 更新后这个错误自然会消失
-
-
ConditionalEffect : Effect (implies)
它也是一种效果,但是一种和其它效果组合产生的效果,它所表达的含义是:使其它效果条件化。它是通过 SimpleEffect 的 implies 函数来实现的
kotlin
@ContractsDsl
@ExperimentalContracts
@SinceKotlin("1.3")
public interface SimpleEffect : Effect {
@ContractsDsl
@ExperimentalContracts
public infix fun implies(booleanExpression: Boolean): ConditionalEffect
}
也就是: simpleEffect implies (booleanExpression: Boolean) 如果函数的某个 SimpleEffect (Returns || Returns(value) || ReturnsNotNull )成立,那么意味着 后面的 boolean 表达式就是成立的,可以看到这里的 implies 函数返回的就是ConditionalEffect 。这个整体所表达的就是一个 ConditionalEffect (条件化的效果)。
CallsInPlace : Effect
它的含义是:对于带有高阶函数(函数类型参数,也就是Lambda)参数的函数,开发者可以告诉编译器这个函数中的某个高阶函数 在当前这个函数中被调用了,并且可以指定被调用的次数(通过 InvocationKind 枚举)。
InvocationKind:lambda 在函数中被调用的次数,包括 EXACTLY_ONCE(有且仅有一次)、AT_MOST_ONCE(至多一次)、AT_LEAST_ONCE(至少一次)、UNKNOWN(未知,默认)。
使用场景:
- 你希望接收一个 lambda,并希望编译器知道这个 lambda 会被立即/一次性执行,从而改进对在 lambda 中进行变量初始化分析
- 在 kotlin 标准库中的
run,apply,also,let这类控制流函数就使用了 callsInPlace()
示例:
kotlin
@OptIn(ExperimentalContracts::class)
inline fun initSafely(block: () -> Unit) {
contract {
//这里告诉编译器,将保证在当前这个函数内部调用且仅调用一次 block
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block()
}
fun initExample() {
val message: String?
initSafely {
message = "Hello, World!" // 编译器知道这个 lambda 会执行一次
}
println("message is $message") // 可以安全使用
}
PS: 注意,在 Kotlin 1.3 官方文档关于 CallsInPlace 的示例中(kotlinlang.org/docs/whatsn...),有一个明显错误,它在
callsInPlace{}中指定了EXACTLY_ONCE但是在 contract 外部却没有调用 block,这应该是笔误,大家如果看到不要被误导!
kotlinfun synchronize(lock: Any?, block: () -> Unit) { // It tells the compiler: // "This function will invoke 'block' here and now, and exactly one time" contract { callsInPlace(block, EXACTLY_ONCE) } //*没有实际调用 block()!!!* } fun foo() { val x: Int synchronize(lock) { x = 42 // Compiler knows that lambda passed to 'synchronize' is called // exactly once, so no reassignment is reported } println(x) // Compiler knows that lambda will be definitely called, performing // initialization, so 'x' is considered to be initialized here }
HoldsIn : Effect
这是 Kotlin v2.2.20 中新增的一个重要的 Effect,它表示在带有高阶函数(lambda)参数的函数中,开发者告诉编译器:我能保证在传入的这个 lambda 中,某个 boolean 表达式的结果为true!,然后编译器就可以在这个 lambda 内部智能的认为 condition 为 true,而不会再需要开发者在 lambda 内部进行 condition 的条件判断,它通常和上面的 callsInPlace 结合使用
语法:condition holdsIn block
kotlin
@OptIn(ExperimentalContracts::class, ExperimentalExtendedContracts::class)
fun <T> T.alsoIf(condition: Boolean, block: (T) -> Unit): T {
contract {
// 声明这个 lambda 在这个函数中最多被调用一次
callsInPlace(block, InvocationKind.AT_MOST_ONCE)
// 声明在lambda内部假设条件为真
condition holdsIn block
}
//开发者来实际自己保证在contract中的承诺
if (condition) block(this)
return this
}
fun useApplyIf(input: Any) {
val result = listOf(1, 2, 3)
.first()
.alsoIf(input is Int) {
// 输入参数在lambda内部被智能转换为Int,可以直接和列表中第一个元素相加
println("holdIn:${input + it}")
}
.toString()
}
这也是Kotlin 2.2.20 中的一个实验的扩展功能,因此也必须额外添加 @OptIn(ExperimentalExtendedContracts::class) 注解
同时也须要在build.gradle(.kts) 中添加额外的编译器选项:
kotlin
kotlin {
compilerOptions {
freeCompilerArgs.add("-Xallow-holdsin-contract")
}
}
⚠️ PS:同样的,这与
condition implies returnsNotNull()的问题一样,在当前最新的 AS 中仍然会报错,如果配置正确,可以忽略 IDE 报错警告!
Kotlin 2.2.20 关于 Contracts 的其它新特性
支持在属性访问器和操作符中使用 Contracts
将 Contracts 的使用扩展到属性的getter方法和以下操作符 中
invokecontainsrangeTo,rangeUntilcomponentNiteratorunaryPlus,,unaryMinusnotinc,dec
示例(getter):
kotlin
val Any?.isNonNullString: Boolean
get() {
@OptIn(ExperimentalContracts::class)
contract { returns(true) implies (this@isNonNullString is String) }
return this is String
}
fun useProp(obj: Any?) {
if (obj.isNonNullString) {
// obj 智能转换为 String
println(obj.length)
}
}
示例(invoke):
kotlin
class Runner {
@OptIn(ExperimentalContracts::class)
operator fun invoke(block: () -> Unit) {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block()
}
}
fun testOperator(runner: Runner) {
val number: Int
runner {
number = 1
}
//
println(number)
// 1
}
同样的,要启用这些功能也需要添加额外的编译器选项:
kotlin
kotlin {
compilerOptions {
freeCompilerArgs.add("-Xallow-contracts-on-more-functions")
}
}
泛型类型断言
由于泛型类型擦除的特性,因此在这之前我们无法 写出 value is List<String> 这种代码,现在Kotlin 2.2.20 的 Contracts 新增了 泛型类型断言 这一功能,因此我们就可以在 contract{ } 内部写出这样的代码了。
通过contracts,你可以在一个函数里声明:如果函数返回某值 (比如 true) 或发生某 effect,那么 this 或函数参数在类型上满足某个泛型类型。
示例:
kotlin
sealed class Failure {
class HttpError(val code: Int) : Failure()
// Insert other failure types here
}
sealed class Result<out T, out F : Failure> {
class Success<T>(val data: T) : Result<T, Nothing>()
class Failed<F : Failure>(val failure: F) : Result<Nothing, F>()
}
@OptIn(ExperimentalContracts::class)
fun <T, F : Failure> Result<T, F>.isHttpError(): Boolean {
contract {
// 使用Contracts 来断言泛型类型, 此处编译器将不再报错(仅限于 contract 内部)
returns(true) implies (this@isHttpError is Result.Failed<Failure.HttpError>)
}
return this is Result.Failed && this.failure is Failure.HttpError
}
同样的,要启用这些功能也需要添加额外的编译器选项(与操作符中的编译选项一致)
kotlin
kotlin {
compilerOptions {
freeCompilerArgs.add("-Xallow-contracts-on-more-functions")
}
}
⚠️ PS: 与 其它 Kotlin 2.2.20新增的特性一样,在当前最新的 AS 中泛型断言特性,IDE仍然会 提示错误:
Error in contract description: instance check for erased type.如果配置正确,可以忽略 IDE 报错警告!
上面的示例的含义是:开发者告诉编译器,我确定调用 isHttpError 的这个 result 是 Result.Failed<Failure.HttpError>的一个实例
这个断言不是真在运行时检查类型 (像 reified 那样),而是 编译时 (或静态分析) 提示编译器 "我承诺这种类型关系在满足合约时成立"。编译器据此可以做智能推断 、类型安全分析等。
与 reified 的区别 :
先简单介绍下 reified :
由于泛型类型擦除的原因,在普通 (non-inline) 函数里 你无法在运行时做 value is T 这样的类型检查 ,因为 T 在运行时的信息不可用。
-
Kotlin 引入了
reified关键字 +inline函数来"实化"类型参数。所谓实化,就是编译器在调用处把函数展开 (inline),并用实际类型把类型参数替换。这样在调用点,类型是具体的,而不是泛型参数。 -
举例:
kotlininline fun <reified T> isType(item: Any): Boolean { return item is T }当你调用
isType<String>("hello")时,编译器把这一调用处内联展开,变成类似"hello" is String,类型检查就能在运行时执行。 -
限制:
reified只能和inline一起使用,因为需要将函数展开- 即便如此,你也不能对 带泛型参数 完全检查所有层级。例如,你不能在 reified 函数中安全地做
value is List<String>,因为泛型参数<String>仍被擦除,你可以检查的是List<*>这样的原始类型
因此,inline + reified 与 Contracts 的泛型类型断言 解决的问题是不相同的,是两个无关的功能
至此,截至最新的 Kotlin ,Contras 的所有功能已经讲述完毕!
自定义 Contracts
Kotlin 标准库内部已经在很多基础函数中使用了 Contracts ,即使大家不了解这个特性,但可能早就已经在项目中间接用上了这个特性。
上面我们已经了解了基于最新版 Kotlin 中 Contracts 的所有特性,其实我们已经发现了 自定义 Contracts 的一些要求和限制,需要注意什么、能做什么、不能做什么,现在总结一下:
-
实验性:
@OptIn(ExperimentalContracts::class)- Kotlin Contracts 是实验性的(Experimental),所以在你自定义的 Contracts 所在的函数上面必须声明:
@OptIn(ExperimentalContracts::class)注解。 - 另外,如果使用Kotlin 2.2.20 中新增的函数(
condition implies returnsNotNull()和holdsIn) 需要额外再添加@OptIn(ExperimentalExtendedContracts::class)注解,并且Kotlin 2.2.20 所有的新特性都要添加对应的 编译器选项。 - 由于是实验特性,其语法、行为在未来 Kotlin 版本可能发生变动。
- 编译器是无条件信任合法的 Contracts 的,不会在运行时强制验证你的合约是否真的被满足,因此开发者需要自己负责使用了自定义的Contracts 的函数中的逻辑正确性。
- Kotlin Contracts 是实验性的(Experimental),所以在你自定义的 Contracts 所在的函数上面必须声明:
-
Contracts 块的位置和语法要求:
-
contract { ... }必须是函数体内的第一条语句 -
contract { ... }内部只能使用ContractBuilder接口中定义了的effect 方法以及 继承了 Effect 接口的所有 Effect (returns(),returns(value:Any?),returnsNotNull(),simpleEffect implies condition,condition implies returnsNotNull(),callsInPlace(...),holdsIn) -
contract { ... }内只能引用当前函数的参数,不能引用局部变量和其它作用域变量kotlinfun example(param1: String?, param2: Int) { contract { // ✅ 只能引用函数参数 returns() implies (param1 != null) // ❌ 不能引用局部变量或其他作用域的变量 // returns() implies (localVar != null) // 错误 } val localVar = "test" } -
类型检查表达式限制:
kotlinfun example(obj: Any?) { contract { // ✅ 允许:简单的类型检查 returns() implies (obj is String) returns() implies (obj != null) // ❌ 限制:复杂的表达式 // returns() implies (obj.toString().length > 5) // 错误 // returns() implies (someFunction(obj)) // 错误 } } -
Contract {} 内的代码只会影响编译器,不会影响运行时,因为
contract { ... }本身在字节码中没有运行时逻辑 (即标准库中contract { ... }是空实现) ,它是给编译器看的 DSL
-
-
函数类型限制:
kotlin// ✅ 支持:顶层函数、成员函数、扩展函数 // 以及 Kotlin2.2.20 中新增的 getter和操作符函数(参见 2.2.20 Contracts 新特性) fun topLevelFunction() { /* ... */ } class MyClass { fun memberFunction() { /* ... */ } } fun String.extensionFunction() { /* ... */ } // ❌ 不支持:局部函数、匿名函数、lambda 表达式 fun outerFunction() { fun localFunction() { contract { /* ... */ } // 编译错误 } }
以上是就是我对 Kotlin Contracts 的所有讲解,希望能帮到大家,Kotlin Contracts 是一个相比其它特性较为冷门的高级特性,并且对于自定义 Contracts 至今仍然是 实验性的,大家有什么自己的见解或者对本文内容有不同的理解,欢迎讨论!