Kotlin contract 用法及原理

什么是 contract

contract(契约)是一种 Kotlin 面向编译器约定的一种规则,它帮助编译器更加智能地识别某些需要特定的代码条件,为代码创建更加友好的上下文关联环境。
Kotlin 在 1.3 版本以实验室功能的方式开始引入 contract, 截止至当前 Kotlin 最新版本 1.6.10,contract 方法依然添加有 @ExperimentalContracts 注解。
这说明官方认为其能力还不够稳定,但从 Kotlin 标准库已经随处可以看见 contract 的情况下看,contract 应该很快就会变为 release feature,我们可以先了解一下 contract 的能力和用法。

常见的 contract 调用

Standart.kt

@kotlin.internal.InlineOnly
public inline fun <T> T.apply(block: T.() -> Unit): T {
    contract { // 告诉编译器,这个 block 一定会执行一次
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block()
    return this
}

@kotlin.internal.InlineOnly
public inline fun <T, R> T.let(block: (T) -> R): R {
    contract { // 告诉编译器,这个 block 一定会执行一次
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block(this)
}

引入 contract 的原因

Kotlin 编译器很强大,从它可以编译成 java 字节码和 JavaScript 就可以看出。但是再强大的编译器也有他的局限性(严格意义上甚至说不上是局限性,只是一种当前最优方案的折中做法),其中就包括智能类型转换和空判断,先举个例子:

// 声明一个抽象父类
abstract class Animal

// 子类 Bird 继承自 Animal
class Bird : Animal() {
    fun fly() {
        // do fly
    }
}

在测试类里面进行智能类型转换

class Tester {
    fun move(animal: Animal) {
        if (animal is Bird) {
            animal.fly() // 由于前面已经做过类型判断,所以这里可以进行类型的智能转换
        }
    }
}

但是如果把这个智能类型转换抽到一个方法中,这种智能转换就无法关联到代码上下文中

class Tester {
    private fun isBird(animal: Animal) = animal is Bird
    
    fun move(animal: Animal) {
        if (isBird(animal)) { // 即使这里返回 true ,编译器无法传递当前 animal 是 Bird 类型
            animal.fly() // 这里会报编译错误,无法找到 fly() 方法
        }
    }
}

在一个看似很简单的判断中,编译器就在一个方法中就已经无法传递 animal 的类型。当然这个只是对于我们调用者来说,编译器难道就做不到吗?答案是 可以但没必要,这是因为

第一,函数的执行结果无法直接给到编译时使用。再者,以 isBird(animal) 为例,即使编译时可以传递 animal 的具体类型,但是如果 isBird(animal) 嵌套多层调用,且调用的函数实现复杂的话,这样编译器就需要花费更多的资源去分析上下文并传递类型,这会造成了编译时长和资源占用的增加

问题是调用者已经明明可以很轻易的知道在这个上下文中,类型已经是可以被指定的,编译器却无法识别。contract 来了,它就是为了解决这种问题而诞生的,有了它,我们就可以在编译阶段 显式地 告诉编译器在特定条件下
得到我们期望的上下文数据,而不需要编译器再自行去做代码检查

contract 使用

在上面的例子中,我们就可以通过 contract 调用来让 animal 是 Bird 这个类型在单独的方法判断后可以继续向下传递

class Tester {
    @ExperimentalContracts // 定义 contract 处需要加上实验性质的注解
    private fun isBird(animal: Animal): Boolean {
        contract { // 定义一个 contract,表示如果 isBird 这个方法返回 true,那么意味着 animal 就是 Bird 类型
            returns(true) implies (animal is Bird)
        }
        return animal is Bird
    }

    @ExperimentalContracts // 调用含有 contract 的方法也需要加入实验性质的注解
    fun move(animal: Animal) {
        if (isBird(animal)) {
            animal.fly() // isBird 返回 true,这里已经让编译器知道 animal 就是 Bird 类型
        }
    }
}

通过加入 contract 后,此时在调用 isBird(animal) 返回 true 后,编译器已经知道 animal 就是 Bird 类型,达到了只能类型转换在抽离到独立方法后,依然可以向下传递的作用,这样可以让代码更加地趋向了自然语言,提高了代码的可读性。
而事实上,contract 本身就是 DSL 方式声明的

而通过转换 Kotlin 代码成 java 发现,在加入 contract 后,kotlin 是将 animal 强转成了 Bird 类型

public final class Tester {
   @ExperimentalContracts
   private final boolean isBird(Animal animal) {
      return animal instanceof Bird;
   }

   @ExperimentalContracts
   public final void move(@NotNull Animal animal) {
      Intrinsics.checkNotNullParameter(animal, "animal");
      if (this.isBird(animal)) {
         ((Bird)animal).fly(); // 加入 contract 后,kotlin 编译器在此处将 animal 强转为了 Bird
      }

   }
}

contract 原理

先来看下 contract 的定义

ContractBuilder.kt

/**
 * 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 是一个内联方法,参数是一个 ContractBuilder 的扩展函数,ContractBuilder 暴露了 4 个接口,分别是

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
}

4 个接口返回的类型都为 kotlin.contracts.Effect 的子类,主要可以分为两大部分:

  1. Returns 和 ReturnsNotNull 代表的是返回结果的效果导向 contract
  2. CallsInPlace 则代表的是执行次数的效果导向 contract

返回结果的效果导向 contract

在前面的 demo 中的是返回结果导向的例子,回头再看下代码

    @ExperimentalContracts // 定义 contract 处需要加上实验性质的注解
    private fun isBird(animal: Animal): Boolean {
        contract { // 定义一个 contract,表示如果 isBird 这个方法返回 true,那么意味着 animal 就是 Bird 类型
            returns(true) implies (animal is Bird)
        }
        return animal is Bird
    }

returns(true) implies (animal is Bird) 很轻易地看出 contract 的 DSL 声明方式。往下看 returns(true) 的实现

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

返回一个 Returns 对象,Returns 继承自 SimpleEffect,SimpleEffect 继承自 Effect。看下 SimpleEffect 的定义

/**
 * 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
}

它代表一个 contract 所在方法执行后对编译器的影响,而通过中缀方法 implies 告诉编译器 booleanExpression 表达式将成立,再一次看前面 demo 的 contract 调用

returns(true) implies (animal is Bird)

这段代码会告诉编译器,当调用 contract 所在这个方法返回 true 时意味着 animal 这个对象就是 Bird 类型,在往后的编译中,编译器将遵守这个约定。通过这样,就弥补了智能类型转换结果在方法中也不可以向下传递的短板

执行次数的效果导向 contract

这种 contract 在 Kotlin 标准库中是最常见的 contract,我们以 apply 函数为例

/**
 * Calls the specified function [block] with `this` value as its receiver and returns `this` value.
 *
 * For detailed usage information see the documentation for [scope functions](https://kotlinlang.org/docs/reference/scope-functions.html#apply).
 */
@kotlin.internal.InlineOnly
public inline fun <T> T.apply(block: T.() -> Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block()
    return this
}

先解释下这里 contract 的作用:它将表明 block 这个扩展方法一定会在这里被执行一次

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

callsInPlace 同样作为 ContractBuilder 的成员方法,参数为两个,分别是:

  • lambda: 需要标记会执行的 block
  • kind: 上面 block 将会被执行的次数,kind 有四种类型
public enum class InvocationKind {

    @ContractsDsl AT_MOST_ONCE, // 至多一次

    @ContractsDsl AT_LEAST_ONCE, // 至少一次

    @ContractsDsl EXACTLY_ONCE, // 有且只有一次

    @ContractsDsl UNKNOWN // 未知,默认值
}

callsInPlace 的返回值为 CallsInPlace,同样也是继承自 Effect。编译器通过该 contract 得以知道扩展方法 block 的被执行情况。举个简单的例子:

class Tester {
    @ExperimentalContracts
    fun test() {
        var strObject: String
        initStr {
            strObject = "This field must be initialized"
        }
        strObject.length // 编译错误,编译器不知道 strObject 是否有被初始化
    }


    @ExperimentalContracts
    fun initStr(block: () -> Unit) {
        block()
    }
}

上面的方法将编译错误,报错信息如下

Variable 'strObject' must be initialized 

那是因为编译器不知道 initStr 方法中的 block 有没有被执行,也即是不知道 strObject 有没有被初始化。这时可以加个 contract

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

这样就顺利编译通过,因为此时,编译器已经被 contract 告知,initStr 方法中的 block 一定会被执行一次

总结

contract 为开发者解决了编译器不够智能的问题,这样可以使代码更简练,更加通俗易懂。但是这个智能的做法是通过开发者主观代码告诉编译器的,编译器无条件地遵守这个约定,这也就为开发者提出了
额外的要求,那就是一定要确保 contract 的正确性,不然将会导致很多不可控制的错误,甚至是崩溃。

还是以最开始的 demo 为例子,虽然不是特别恰当,当仍然说明了上面所说的风险:

class Tester {
    private fun isBird(animal: Animal) = animal is Bird
    
    fun move(animal: Animal) {
        if (isBird(animal)) { 
            animal.fly()
        } else { // 由于 contract 的错误逻辑,将导致下面代码的崩溃
            animal.swin()
        }
    }
}

我们假设当前传入的 animal 的实际对象是 Dog,而此时,如果在 isBird() 方法中 contract 错误,就有可能导致崩溃。

    @ExperimentalContracts // 
    private fun isBird(animal: Animal): Boolean {
        contract { // 注意看这里,为了试验,写了一个低级的错误,当 return false 时,代表当前的 animal 为是 Fish
            returns(false) implies (animal is Finish)
        }
        return animal is Bird
    }

导致 move 方法中走到 else 分支,而当前 Dog 是没有 swin 方法的,这样就崩溃了

  • 11
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Kotlin Flow是一种用于异步数据流处理的库,它提供了一种类似于RxJava的响应式编程模型。Flow的实现原理主要涉及以下几个方面: 1. Flow的基本概念:Flow是一种冷流(Cold Stream),它是一种异步的、可取消的序列。Flow中的数据是按需产生的,只有当有收集器订阅时,才会开始产生数据。 2. Flow的操作符:Flow提供了一系列的操作符,用于对数据流进行转换、过滤、合并等操作。这些操作符是惰性的,只有当有收集器订阅时,才会触发数据的处理。 3. Flow的调度器:Flow可以通过调度器指定数据流的执行线程。调度器可以将数据流的处理切换到指定的线程池或协程上下文中,以实现并发执行或避免阻塞主线程。 4. Flow的背压支持:Flow提供了背压支持,可以通过调整缓冲区大小或使用缓存策略来处理生产者和消费者之间的速度不匹配问题。 5. Flow的异常处理:Flow可以通过catch操作符捕获异常,并在异常发生时执行特定的逻辑。此外,Flow还提供了retry和retryWhen操作符,用于处理异常重试。 6. Flow的取消支持:Flow可以通过取消协程来终止数据流的产生和处理。当取消流时,Flow会自动取消相关的协程,以释放资源并停止数据的产生。 总结起来,Kotlin Flow的实现原理主要涉及冷流、操作符、调度器、背压支持、异常处理和取消支持等方面。通过理解这些原理,可以更好地使用和理解Kotlin Flow库。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值