Kotlin contract 用法及原理

Quibbler 6月前 540

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
    }

        有三种返回类型:ReturnsReturnsNotNullCallsInPlace,均继承自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_ONCEAT_LEAST_ONCEEXACTLY_ONCEUNKNOWN

    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
    }


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


不忘初心的阿甘
最新回复 (0)
    • 安卓笔记本
      2
        登录 注册 QQ
返回
仅供学习交流,切勿用于商业用途。如有错误欢迎指出:fluent0418@gmail.com