有趣的 Kotlin 0x0C:Contracts

780 阅读3分钟

Kotlin 语言中深受开发者喜爱特性之一就是 智能推断

fun foo(s: String?) {
    if (s != null) s.length // Compiler automatically casts 's' to 'String'
}

如上,编译器会自动推断出变量 s 为非空 String 类型。

但是在一些小把戏面前,Kotlin 的智能推断又没有想象中的那么强大。下面这段代码,编译器并没有完成智能推断。

fun String?.isNotNull(): Boolean = this != nullfun foo(s: String?) {
    if (s.isNotNull()) s.length // No smartcast :(
}

我们再换一种写法,编译器则又能帮助我们完成智能推断

fun bar(x: String?) {
    if (!x.isNullOrEmpty()) {
        println("length of '$x' is ${x.length}") // Yay, smartcast to not-null!
    }
}

这里,我们使用的是 Kotlin 标准库下 CharSequence? 的扩展函数 isNullOrEmpty()

@kotlin.internal.InlineOnly
public inline fun CharSequence?.isNullOrEmpty(): Boolean {
    contract {
        returns(false) implies (this@isNullOrEmpty != null)
    }
​
    return this == null || this.length == 0
}

源码中出现 contract 函数,即 契约,此处体现的是开发者和编译器之间的契约。开发者使用 contract 函数告诉编译器,当 isNullOrEmpty() 函数返回 false 时,则 this@isNullOrEmpty != null 成立,帮助编译器更好地完成静态代码的分析,包括智能推断。

概念

Contracts,契约机制是为了让开发者告诉编译器有用的信息以便帮助它更好地完成代码分析,而在 Kotlin 1.3 中引入的实验机制。目前语法还处于实验状态,但是在 Kotlin 标准库中已有大量使用。简而言之,Kotlin Contracts 是一种通知编译器有关函数行为的方式。目前主要在下面两类场景中广泛使用:

  • Returns Contracts:通过声明函数的调用结果和传递的参数值之间的关系来改进智能推断
fun require(condition: Boolean) {
    // This is a syntax form which tells the compiler:
    // "if this function returns successfully, then the passed 'condition' is true"
    contract { returns() implies condition }
    if (!condition) throw IllegalArgumentException(...)
}
​
fun foo(s: String?) {
    require(s is String)
    // s is smartcast to 'String' here, because otherwise
    // 'require' would have thrown an exception
}
  • CallInPlace Contracts:在存在高阶函数的情况下改进变量初始化分析
fun 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) }
}
​
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
}

Returns Contracts

Returns Contracts 表示当 return 的返回值是某个值(例如true、false、null)时,implies 后面的条件成立。

Returns Contracts 主要有如下几种形式:

  • returnsContract
  • returnsTrueContract
  • returnsFalseContract
  • returnsNullContract
  • returnsNotNullContract

Kotlin 官方提供示例以供学习。

// 若函数能正常返回,则 condition 必然为 true 
@kotlin.contracts.ExperimentalContracts
fun returnsContract(condition: Boolean) {
    contract {
        returns() implies (condition)
    }
    if (!condition) throw IllegalArgumentException()
}
​
// 若函数返回 true, 则 condition 必然为 true
@kotlin.contracts.ExperimentalContracts
fun returnsTrueContract(condition: Boolean): Boolean {
    contract {
        returns(true) implies (condition)
    }
    return condition
}
​
// 若函数返回 false, 则 condition 必然为 true
@kotlin.contracts.ExperimentalContracts
fun returnsFalseContract(condition: Boolean): Boolean {
    contract {
        returns(false) implies (condition)
    }
    return !condition
}
​
// 若函数返回 null, 则 condition 必然为 true
@kotlin.contracts.ExperimentalContracts
fun returnsNullContract(condition: Boolean): Boolean? {
    contract {
        returns(null) implies (condition)
    }
    return if (condition) null else false
}
​
// 若函数返回不为空, 则 condition 必然为 true
@kotlin.contracts.ExperimentalContracts
fun returnsNotNullContract(condition: Boolean): Boolean? {
    contract {
        returnsNotNull() implies (condition)
    }
    return if (condition) true else null
}

举例说明,帮助编译器智能推断对象类型

class Cat {
    fun greeting(){
        println("Hello, Little Cat!!")
    }
}
​
@ExperimentalContracts
fun guess(sth: Any?) {
    if (isCat(sth)) {
       sth.greeting() // 智能推断 std 的类型为 Cat 
    }
}
​
@ExperimentalContracts
fun isCat(any: Any?): Boolean {
    contract {
        returns(true) implies (any is Cat)
    }
    return any is Cat
}

CallInPlace Contracts

@ContractsDsl public fun <R> callsInPlace(lambda: Function<R>, kind: InvocationKind = InvocationKind.UNKNOWN): CallsInPlace
public enum class InvocationKind {
    @ContractsDsl AT_MOST_ONCE,  // 最多调用一次
    @ContractsDsl AT_LEAST_ONCE, // 最少调用一次
    @ContractsDsl EXACTLY_ONCE,  // 只调用一次
    @ContractsDsl UNKNOWN        // 未知
}

同样,Kotlin 官方提供示例以供学习。

@kotlin.contracts.ExperimentalContracts
inline fun callsInPlaceAtMostOnceContract(block: () -> Unit) {
    contract {
        callsInPlace(block, InvocationKind.AT_MOST_ONCE)
    }
}
​
@kotlin.contracts.ExperimentalContracts
inline fun callsInPlaceAtLeastOnceContract(block: () -> Unit) {
    contract {
        callsInPlace(block, InvocationKind.AT_LEAST_ONCE)
    }
    block()
    block()
}
​
@kotlin.contracts.ExperimentalContracts
inline fun <T> callsInPlaceExactlyOnceContract(block: () -> T): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block()
}
​
@kotlin.contracts.ExperimentalContracts
inline fun callsInPlaceUnknownContract(block: () -> Unit) {
    contract {
        callsInPlace(block, InvocationKind.UNKNOWN)
    }
    block()
    block()
    block()
}

举例说明,告诉编译器 action 肯定会执行一次。

fun hello(){
    val hello:Int
    once {
        hello =  2   // 确保变量赋值过程只会执行一次
    }
    println(hello)
}
​
@OptIn(ExperimentalContracts::class)
fun once(action:()->Unit){
    contract {
        callsInPlace(action,InvocationKind.EXACTLY_ONCE)
    }
    action.invoke()
}

总结

Contracts 是开发者与编译器沟通的通道,可以帮助编译器更好地完成静态代码的分析。但是无形间也对开发者提出了编码要求,因为编译器不会验证 Contracts,只会无条件的执行,且当前 API 处于实验阶段,若非必要,不建议使用,知道有这么回事即可。