Kotlin Contracts 契约机制

深入学习Kotlin基础知识
2022-07-07 10:47 · 阅读时长6分钟
小课

Kotlin Contracts是kotlin 1.3推出的一个新特性,它主要作用是进一步提升kotlin编译器的智能转换(smartcasts)能力,先看看下面这个示例。

fun foo(s: String?) {
    if (!= null) {
        println("length of '$s' is ${s.length}") //编译器自动将s转换为String类型
    }
}

这样很符合逻辑,其实这背后是kotlin编译器帮我们做了智能转换,但是并不是所有的情况编译器都能这样默默的处理了,比如说,我们把判空处理放在另外一个独立的方法中。

1fun main() {
2    foo("hello kotlin")
3}
4
5fun isNotNull(s: String?) = s != null
6
7fun foo(s: String?) {
8    if (isNotNull(s)) {
9        println("length of '$s' is ${s.length}") //编译出错 Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type String?
10    }
11}

仅仅是把功能封装成了一个单独的方法,之前的智能转换功能居然不生效了,这对于开发者来说很容易推断出s可以转换为String类型而不是String?,但是对于编译器来说却比较麻烦,为了增强这类场景kotlin编译器的智能转换能力,所以推出了Contracts机制。

我们将以上代码判断字符串的部分改为kotlin标准库中的扩展方法isNullOrBlank,则可以顺利编译通过。

fun main() {
    foo("hello kotlin")
}

fun foo(s: String?) {
    if (!s.isNullOrBlank()) {
        println("length of '$s' is ${s.length}")
    }
}
通过声明函数调用的结果与所传参数值之间的关系来改进智能转换分析

下面是isNullOrBlank方法的定义,从定义中我们可以看出它使用了Contracts机制。

public inline fun CharSequence?.isNullOrBlank(): Boolean {
    contract {
        returns(false) implies (this@isNullOrBlank != null)
    }
    return this == null || this.isBlank()
}

后面是正常的判空处理,重点是contract这一段代码,它代表合约的内容:如果方法返回true,表明字符串实例不是null。编译器可以根据这个约定来决定是否可以将字符串s实例从String?类型转换为String类型。我们将上面提到编译出错的代码改动一下,使用Contracts机制来帮助编译器完成智能转换。

1import kotlin.contracts.ExperimentalContracts
2import kotlin.contracts.contract
3
4@ExperimentalContracts
5fun main() {
6    foo("hello kotlin")
7}
8
9@ExperimentalContracts
10fun isNotNull(s: String?): Boolean {
11    contract {
12        returns(true) implies (s != null)
13    }
14    return s != null
15}
16
17@ExperimentalContracts
18fun foo(s: String?) {
19    if (isNotNull(s)) {
20        println("length of '$s' is ${s.length}")
21    }
22}

除了returns(value) implies(condition)这种形式,还有以下几种。

  • returns() implies(condition),如果方法调用正常返回,则表明condition成立。
  • returnsNotNull() implies(condition),如果方法返回值不为null,则表明condition成立。
在存在高阶函数的情况下改进变量初始化的分析

除了能够帮助编译器智能转换以外,Contracts机制还能帮助编译器在存在高阶函数时,判断一个变量是否初始化。先看看下面示例,因为编译器无法判断变量是否会初始化,所以导致编译出错。

1fun main() {
2    foo()
3}
4
5fun bar(block: () -> Unit) {
6    block()
7}
8
9fun foo() {
10    var x: Int
11    bar {
12        x = 42
13    }
14    println(x)
15}

从开发者角度来看,这个x = 42这行代码会执行一次,也就是说x会在println方法之前赋值,但是对于编译器来说,它却不能很智能的知道这个情况,所以就出现了编译错误。在ContractBuilder中还定义了方法callsInPlace,用于提示编译器传入高阶函数中的表达式的执行情况。

比如说上面的代码,如果我们使用Contracts机制提示编译器,block会被执行一次,那么它就能够知道x变量会在println方法之前被赋值,也就不会出现编译错误了。

1import kotlin.contracts.ExperimentalContracts
2import kotlin.contracts.InvocationKind
3import kotlin.contracts.contract
4
5@ExperimentalContracts
6fun main() {
7    foo()
8}
9
10@ExperimentalContracts
11fun bar(block: () -> Unit) {
12    contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) }
13    block()
14}
15
16@ExperimentalContracts
17fun foo() {
18    var x: Int
19    bar {
20        x = 42
21    }
22    println(x)
23}
kotlincontract契约合约smartcasts