Kotlin contract 用法及原理

QuibblerQuibbler 2024-07-20 约 19 分钟 1613 次阅读

Kotlin contract 用法及原理

在阅读Kotlin源码的时候,会发现很多常用方法里都有contract的身影:

/**
     * 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).
     */
    public inline fun <T> T.apply(block: T.() -> Unit): T {
        contract {
            callsInPlace(block, InvocationKind.EXACTLY_ONCE)
        }
        block()
        return this
    }

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

contract(契约)是一种 Kotlin 面向编译器约定的一种规则,它帮助编译器更加智能地识别某些需要特定的代码条件,为代码创建更加友好的上下文关联环境。

1、contract定义

Kotlin在1.3版本以实验室功能的方式开始引入contract,截止至当前Kotlin最新版本1.6.10,contract方法依然添加有@ExperimentalContracts注解,这说明官方认为其能力还不够稳定。

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

在其构造类ContractBuilder中定义了四种方法:returns()、returns(value: Any?)、returnsNotNull()、callsInPlace(...),方法的作用后面举例子详细说明就会明白它的作用。

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
    }

有三种返回类型:Returns、ReturnsNotNull、CallsInPlace,均继承自Effect接口:

/**
     * Describes a situation when a function returns normally with a given return value.
     *
     * @see ContractBuilder.returns
     */
    public interface Returns : SimpleEffect

    /**
     * Describes a situation when a function returns normally with any non-null return value.
     *
     * @see ContractBuilder.returnsNotNull
     */
    public interface ReturnsNotNull : SimpleEffect

    /**
     * An effect of calling a functional parameter in place.
     *
     * A function is said to call its functional parameter in place, if the functional parameter is only invoked
     * while the execution has not been returned from the function, and the functional parameter cannot be
     * invoked after the function is completed.
     *
     * @see ContractBuilder.callsInPlace
     */
    public interface CallsInPlace : Effect

Returns和 ReturnsNotNull代表的是返回结果的效果导向、CallsInPlace则代表的是执行次数的效果导向。

2、使用场景

为什么需要contract呢?它的作用是什么,继续往下看。声明一个抽象接口File:

abstract class File

子类 PdfFile继承自 File,并且定义了一个专属的子类方法。

class PdfFile: File() {
        fun convertToWord() {
            // do 
        }
    }

在Kotlin中,有自动类型推断,一但判断过类型,后面会自动转换成对应类型使用。这个叫做智能类型转换:Smart cast to PdfFile。

fun move(file: File) {
        if (file is PdfFile) {
            //Smart cast to PdfFile
            file.convertToWord()
        }
    }

但是如果通过方法判断的呢?判断逻辑抽取到独立的方法中:

fun isPDF(file: File): Boolean {
        //逻辑可能更复杂
        return file is PdfFile
    }

由于没有直接上下文,这时候编译器就不能帮我们自动类型转换,会报错:Unresolved reference: convertToWord。

fun move2(file: File) {
        if (isPDF(file)) {
            //Unresolved reference: convertToWord
            file.convertToWord()
        }
    }

虽然编译器完全可以在编译的时候深入方法分析,进而实现类型推断,但是会浪费编译时间,完全没必要。这时候contract契约就出现,通过人为的约定,告诉编译器一些信息。人是会出错的,所以这种方式对开发水平有要求,contract定义的契约一定要可靠。

3、使用contract

这种场景下contract就派上用场了,可以让参数类型在执行的方法上下文传递,方法执行完毕后,继续将类型等信息传递给后续的方法进行编译。

3.1、根据返回结果约定

借助returns()、returns(value: Any?)、returnsNotNull()可以根据方法执行的结果返回约定信息。

@OptIn(ExperimentalContracts::class)
    fun isPDF(file: File): Boolean {
        contract {
            returns(true) implies (file is PdfFile)
        }
        return file is PdfFile
    }

比如在前面的例子里,通过加入contract中的Returns效果,此时在调用 isPDF(file) 返回 true 后,编译器已经知道参数file就是 PdfFile类型,达到了类型转换在抽离到独立方法后,参数类型依然可以向下传递的作用,让代码更加地趋向了自然语言,提高代码的可读性和编码的便利性。

同时需要给调用的方法上也加上注解@ExperimentalContracts,这时候又可以愉快的进行自动类型转换了:Smart cast to PdfFile。

@ExperimentalContracts
    fun move2(file: File) {
        if (isPDF(file)) {
            //Smart cast to PdfFile
            file.convertToWord()
        }
    }

看一下字节码对应的java代码,其实就是编译器根据contract提供的约定条件进行强制类型转换:

@ExperimentalContracts
   public static final void move2(@NotNull File file) {
      Intrinsics.checkNotNullParameter(file, "file");
      if (isPDF(file)) {
          //编译器根据contract提供的约定条件进行强制类型转换
         ((PdfFile)file).convertToWord();
      }
   }

3.2、根据执行次数约定

常用的kotlin方法apply中就使用了contract中的callsInPlace约定,标识该lambda只会执行一次。

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

编译器通过该 contract 得以知道扩展方法 block 的被执行次数的情况。

/**
     * Specifies that the function parameter [lambda] is invoked in place.
     *
     * This contract specifies that:
     * 1. the function [lambda] can only be invoked during the call of the owner function,
     *  and it won't be invoked after that owner function call is completed;
     * 2. _(optionally)_ the function [lambda] is invoked the number of times specified by the [kind] parameter,
     *  see the [InvocationKind] enum for possible values.
     *
     * A function declaring the `callsInPlace` effect must be _inline_.
     *
     */
    @ContractsDsl public fun <R> callsInPlace(lambda: Function<R>, kind: InvocationKind = InvocationKind.UNKNOWN): CallsInPlace

InvocationKind有四种类:AT_MOST_ONCE、AT_LEAST_ONCE、EXACTLY_ONCE、UNKNOWN。

public enum class InvocationKind {
        /**
         * 最多执行一次
         */
        @ContractsDsl AT_MOST_ONCE,
    
        /**
         * 至少执行一次
         */
        @ContractsDsl AT_LEAST_ONCE,
    
        /**
         * 只执行一次
         */
        @ContractsDsl EXACTLY_ONCE,
    
        /**
         * 未知、默认值
         */
        @ContractsDsl UNKNOWN
    }

同样的再举个例子,方便理解callsInPlace的效果。定义了一个方法,接受一个lambda表达式作为参数

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

当借助该方法初始化值时,编译器会报错,因为后续用到了这个值,编译器不知道有没有初始化:Variable 'data' must be initialized。

fun test() {
        var data: String
        
        //初始化放到lambda中执行
        init {
            data = ""
        }
        
        //编译器无法知道是否成功执行初始化
        data.length
    }

这时候就需要用到约定,告诉编译器,该lambda至少会执行一次,相应的变量一定会初始化。

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

给调用的方法也加上@ExperimentalContracts注解,编译器就知道block初始化快会执行一次这个约定,后续再访问data变量就知道已经初始化过了。

@ExperimentalContracts
    fun test() {
        var data: String
        init {
            data = ""
        }
        data.length
    }

由于是人为的约定,编译器也会无条件相信并执行这些约定。所以对开发的水平有要求,不能马虎,约定的场景不能存在漏洞,否则执行过程中会出现异常。

相关推荐

Parcelize:Parcelable 实现生成器
Kotlin

Parcelize:Parcelable 实现生成器

Parcelize:Parcelable 实现生成器Parcelable是安卓开发中常用的序列化方式,之前分享过一个Parcelable序列化插件,方便开发者快速生成代码,提高开发效率。1、Parcelize插件Kotlin官方也推出一个Parcelable自动生成的Gradle插件:Parcelize。1.1、引入插件在项目的build.gradle中添加:id("kotlin-parceliz

2.6k
Kotlin的继承
Kotlin

Kotlin的继承

Kotlin的继承1、超类:Any类似于Java中的Object类,Kotlin也有一个叫Any的超类,是所有类的父类,所有类都默认继承自该类。超类的定义如下,其中定义了三个函数:2、open注意上面的Any类用open修饰,表示该类可以被继承。Kotlin中的类默认都是final的,无法被继承:如果想让类可以被继承,就需要用在定义类的地方用open关键字标识该类可以被继承。3、构造器继承中涉及到

2.1k
 Kotlin基本语法
Kotlin

Kotlin基本语法

Kotlin基本语法了解Kotlin语言的基本语法。1、包kotlin文件以.kt结尾,编译文件还是如此只不过在文件名后面加上Kt后缀。这一点和Java不同,Java源文件.java,编译后为.class文件。1.1、包定义每个kotlin文件的第一行就用关键字package定义该文件所在的包。例:1.2、导包用到其它kotlin文件需要使用关键字import导入,这一点和Java一样。kotli

2.1k