kotlin 契约探究

34 阅读4分钟

现在我们用kotlin开发的过程中,经常使用kotlin的一些标准库函数,如let、apply等,点击去看到这些库函数的实现时,会发现他们有一个共同的特点

@kotlin.internal.InlineOnly
public inline fun <T, R> T.let(block: (T) -> R): R {
    contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
return block(this)
}
@kotlin.internal.InlineOnly
public inline fun <T> T.apply(block: T.() -> Unit): T {
    contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
block()
    return this
}

都有一个contract 的闭包调用,这个是什么呢?其实这个翻译过来就是"契约",在kotlin中就代表着一种约定:告诉编译器,方法传递过来的参数肯定会被执行。

Contract功能

kotlin有一个很实用的功能——智能类型转换,如下代码所示:

open class Car(var wheels: Int = 4)

class Benz : Car() {
    fun run() {}
}

fun startWork(car: Car) {
    if (car is Benz) {
        car.run()
    }
}


fun startWork(car: Car) {
    if (isBenZ(car)) {
        car.run() //IDE提示找不到这个方法
    }
}

我们在if语句里,判断了car的类型之后,就可以直接调用run方法,但是如果把这个if判断语句抽离到一个方法中,智能类型转换就不生效了,在编译时,会提示找不到方法。那么使用contract,就可以消除这个错误提示:

@ExperimentalContracts
fun isBenz(car: Car): Boolean {
    contract {
returns(true) implies (car is Benz)
    }
return car is Benz
}

因为是contract目前依然处于实验阶段,所以要加@ExperimentalContracts注解。

Contract原理

 /**
* Specifies the contract of a function.
*
* The contract description must be at the beginning of a function and have at least one effect.
*
* Only the top-level functions can have a contract for now.
*
*  @param builder the lambda where the contract of a function is described with the help of the [ContractBuilder] members.
*
*/

@ContractsDsl
@ExperimentalContracts
@InlineOnly
@SinceKotlin("1.3")
@Suppress("UNUSED_PARAMETER")
public inline fun contract(builder: ContractBuilder.() -> Unit) { }

查看一下contract源码,contract是指定函数的契约,contract是一个内联方法,参数builder是ContractBuilder的一个拓展方法,ContractBuilder是一个接口,里面有四个方法

 /**
* Provides a scope, where the functions of the contract DSL, such as [returns], [callsInPlace], etc.,
* can be used to describe the contract of a function.
*
* This type is used as a receiver type of the lambda function passed to the [contract] function.
*
*  @see contract
*/
@ContractsDsl
@ExperimentalContracts
@SinceKotlin("1.3")
public interface ContractBuilder {
 
    @ContractsDsl public fun returns(): Returns

    @ContractsDsl public fun returns(value: Any?): Returns

    @ContractsDsl public fun returnsNotNull(): ReturnsNotNull

    @ContractsDsl public fun <R> callsInPlace(lambda: Function<R>, kind: InvocationKind = InvocationKind.UNKNOWN): CallsInPlace
}

这四个方法,可以分为两类:

  1. Returns和ReturnsNotNull一类是以返回值为导向
  2. CallsInPlace是以执行次数为导向

以返回值为导向

以第一类Returns方法来看,之前的源码,returns(true) implies (car is Benz)是运行在ContractBuilder中的DSL语句

@ExperimentalContracts
fun isBenz(car: Car): Boolean {
    contract {
returns(true) implies (car is Benz)
    }
return car is Benz
}

returns继承于SimpleEffect,SimpleEffect继承于Effect

 /**
* Represents an effect of a function invocation,
* either directly observable, such as the function returning normally,
* or a side-effect, such as the function's lambda parameter being called in place.
*
* The inheritors are used in [ContractBuilder] to describe the contract of a function.
*
*  @see ConditionalEffect
*  @see SimpleEffect
*  @see CallsInPlace
*/
@ContractsDsl
@ExperimentalContracts
@SinceKotlin("1.3")
public interface Effect

/**
* An effect that can be observed after a function invocation.
*
*  @see ContractBuilder.returns
*  @see ContractBuilder.returnsNotNull
*/
@ContractsDsl
@ExperimentalContracts
@SinceKotlin("1.3")
public interface SimpleEffect : Effect {
    /**
* Specifies that this effect, when observed, guarantees [booleanExpression] to be true.
*
* Note: [booleanExpression] can accept only a subset of boolean expressions,
* where a function parameter or receiver (`this`) undergoes
* - true of false checks, in case if the parameter or receiver is `Boolean`;
* - null-checks (`== null`, `!= null`);
* - instance-checks (`is`, `!is`);
* - a combination of the above with the help of logic operators (`&&`, `||`, `!`).
*/
@ContractsDsl
    @ExperimentalContracts
    public infix fun implies(booleanExpression: Boolean): ConditionalEffect
}

Effect代表着的是一个contract所在方法运行后对编译器的影响,而通过中缀表达式implies是保证booleanExpression表达式的成立。

returns(true) implies (car is Benz)

所以这句代码就是告诉编译器contract所在的方法返回true时,代表着car is Benz表达式,在之后的编译中,编译器会遵循着这个契约,这就就弥补了智能类型转换结果在方法中也不可以向下传递的短板。

以执行次数为导向

这类contract就是告诉编译器,传入的block()代码肯定会被执行,以apply函数源码为例

@kotlin.internal.InlineOnly
public inline fun <T, R> T.let(block: (T) -> R): R {
    contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
return block(this)
}

调用的CallsInPlace方法,有两个参数,第一个标定了传入的lambda表达式,第二个则是lambda执行的次数,有四种类型

@ContractsDsl
@ExperimentalContracts
@SinceKotlin("1.3")
public enum class InvocationKind {

    @ContractsDsl AT_MOST_ONCE,  //最多一次
    
    @ContractsDsl AT_LEAST_ONCE, //至少一次

    @ContractsDsl EXACTLY_ONCE, //就执行一次

    @ContractsDsl UNKNOWN //不确定
}

举个例子

fun test() {
    var str: String
    changeContent { 
str = "xx"
    }
str.length
}

fun changeContent(block: () -> Unit) {
    block()
}

这里运行会报错,因为编译器不知道changeContent()方法中的block()是否被执行,也就不知道str是否被初始化过,这时可以加一个Contract

@ExperimentalContracts
fun changeContent(block: () -> Unit) {
    contract { 
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
block()
}

编译器就会知道changeContent方法中的block()肯定被执行了一次。

总结

虽然Contract为开发者解决了编译器不够智能的问题,可以使代码更简练,但是这个智能的做法依然是通过开发者告诉编译器的,编译器无条件地遵守这个契约,这也就为开发者提出了额外的要求,那就是一定要确保 contract 的正确性,不然将会导致很多不可控制的错误,甚至是崩溃,所以目前Contract还处于kotlin官方的实验中,期待它“转正”的那天。