Kotlin Contract

pexels-lina-kivaka-3881172.jpg

Kotlin 的智能推断是其语言的一大特色。

智能推断,能够根据类型检测自动转换类型。

但是,智能推断并没有想象中的强大,例如下面的代码就无法进行推断,导致编译失败:

fun String?.isNotNull():Boolean {
    return this!=null && this.isNotEmpty()
}

fun printLength(s:String?=null) {
    if (!s.isNotNull()) {
        println(s.length) // Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type String?
    }

}

因为编译器在处理s.length时,会将 s 推断成value-parameter s: String? = ...并不是 String 类型。智能推断失效了,代码也无法编译。

对上述代码做如下修改,即可编译成功:

fun printLength(s:String?=null) {
    if (!s.isNullOrEmpty()) {
        println(s.length)
    }
}

isNullOrEmpty() 是 Kotlin 标准库中 String 的扩展函数,其源码:

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

    return this == null || this.length == 0
}

我们会发现 isNullOrEmpty() 的源码中包含了contract函数,实际上它会告诉编译器当 isNullOrEmpty() 返回 false 时,则 isNullOrEmpty != null 成立,因此 printLength() 函数中的变量 s 不会为 null。

通过契约,开发者可以向编译器提供有关函数的行为,以帮助编译器对代码执行更完整的分析。

契约就像是开发者和编译器沟通的桥梁,但是编译器必须无条件地遵守契约。

一. Contract 的概念

Contract 是一种向编译器通知函数行为的方法。

Contract 是 Kotlin1.3 的新特性,在当前 Kotlin 1.4 时仍处于试验阶段。

二. Contract 的特性

  • 只能在 top-level 函数体内使用 Contract,不能在成员和类函数上使用它们。
  • Contract 所调用的声明必须是函数体内第一条语句。
  • 目前 Kotlin 编译器并不会验证 Contract,因此开发者有责任编写正确合理的 Contract。

在 Kotlin 1.4 中,对于 Contract 有两项改进:

  • 支持使用内联特化的函数来实现契约
  • Kotlin 1.3 不能为成员函数添加 Contract,从 Kotlin 1.4 开始支持为 final 类型的成员函数添加 Contract(当然任意成员函数可能存在被覆写的问题,因而不能添加)。

当前 Contract 有两种类型:

  • Returns Contracts
  • CallInPlace Contracts

2.1 Returns Contracts

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

Returns Contracts 有以下几种形式:

  • returns(true) implies
  • returns(false) implies
  • returns(null) implies
  • returns implies
  • returnsNotNull implies

其他几个类型按照字面意思很好理解,returns implies 怎么理解呢?

我们来看一下 Kotlin 的 requireNotNull() 函数的源码:

@kotlin.internal.InlineOnly
public inline fun <T : Any> requireNotNull(value: T?): T {
    contract {
        returns() implies (value != null)
    }
    return requireNotNull(value) { "Required value was null." }
}

@kotlin.internal.InlineOnly
public inline fun <T : Any> requireNotNull(value: T?, lazyMessage: () -> Any): T {
    contract {
        returns() implies (value != null)
    }

    if (value == null) {
        val message = lazyMessage()
        throw IllegalArgumentException(message.toString())
    } else {
        return value
    }
}

contract() 告诉编译器,如果调用 requireNotNull 函数后能够正常返回,且没有抛出异常,则 value 不为空。

因此,returns implies 表示当该函数正常返回时,implies后面的条件成立。

Contract 正是通过这种声明函数调用的结果与所传参数值之间的关系来改进 Kotlin 智能推断的效果。

2.2 CallInPlace Contracts

前面Kotlin 如何优雅地使用 Scope Functions曾介绍过 Scope Function,我们来回顾一下 let 函数的源码:

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

contract() 中的 callsInPlace 会告知编译器,lambda 表达式 block 在 let 函数内只会执行一次。在 let 函数被调用结束后,block 将不再被执行。

callsInPlace() 允许开发者提供对调用的 lambda 表达式进行时间/位置/频率上的约束。

callsInPlace() 中的 InvocationKind 是一个枚举类,包含如下的枚举值:

  • AT_MOST_ONCE:函数参数将被调用一次或根本不调用。
  • EXACTLY_ONCE:函数参数将只被调用一次。
  • AT_LEAST_ONCE:函数参数将被调用一次或多次。
  • UNKNOWN:一个函数参数它可以被调用的次数未知。

Kotlin 的 Scope Function 都使用了上述 Contracts。

三. Contract 源码解析

Contract 采用 DSL 方式进行声明,我们来看一下 contract() 函数的源码:

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

通过 ContractBuilder 构建了 Contract,其源码如下:

@ContractsDsl
@ExperimentalContracts
@SinceKotlin("1.3")
public interface ContractBuilder {
    /**
     * Describes a situation when a function returns normally, without any exceptions thrown.
     *
     * Use [SimpleEffect.implies] function to describe a conditional effect that happens in such case.
     *
     */
    // @sample samples.contracts.returnsContract
    @ContractsDsl public fun returns(): Returns

    /**
     * Describes a situation when a function returns normally with the specified return [value].
     *
     * The possible values of [value] are limited to `true`, `false` or `null`.
     *
     * Use [SimpleEffect.implies] function to describe a conditional effect that happens in such case.
     *
     */
    // @sample samples.contracts.returnsTrueContract
    // @sample samples.contracts.returnsFalseContract
    // @sample samples.contracts.returnsNullContract
    @ContractsDsl public fun returns(value: Any?): Returns

    /**
     * Describes a situation when a function returns normally with any value that is not `null`.
     *
     * Use [SimpleEffect.implies] function to describe a conditional effect that happens in such case.
     *
     */
    // @sample samples.contracts.returnsNotNullContract
    @ContractsDsl public fun returnsNotNull(): ReturnsNotNull

    /**
     * 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 amount of times specified by the [kind] parameter,
     *  see the [InvocationKind] enum for possible values.
     *
     * A function declaring the `callsInPlace` effect must be _inline_.
     *
     */
    /* @sample samples.contracts.callsInPlaceAtMostOnceContract
    * @sample samples.contracts.callsInPlaceAtLeastOnceContract
    * @sample samples.contracts.callsInPlaceExactlyOnceContract
    * @sample samples.contracts.callsInPlaceUnknownContract
    */
    @ContractsDsl public fun <R> callsInPlace(lambda: Function<R>, kind: InvocationKind = InvocationKind.UNKNOWN): CallsInPlace
}

returns()、returnsNotNull()、callsInPlace() 分别返回 Returns、ReturnsNotNull、CallsInPlace 对象。这些对象最终都实现了 Effect 接口:

@ContractsDsl
@ExperimentalContracts
@SinceKotlin("1.3")
public interface Effect

Effect 表示函数调用的效果。每当调用一个函数时,它的所有效果都会被激发。编译器将收集所有激发的效果以便于其分析。

目前 Kotlin 只支持有 4 种 Effect:

  • Returns: 表示函数成功返回,不会不引发异常。
  • ReturnsNotNull:表示函数成功返回不为 null 的值。
  • ConditionalEffect:表示一个效果和一个布尔表达式的组合,如果触发了效果,则保证为true。
  • CallsInPlace:表示对传递的 lambda 参数的调用位置和调用次数的约束。

四. 小结

Contract 是帮助编译器分析的一个很好的工具,它们对于编写更干净、更好的代码非常有帮助。在使用 Contract 的时候,请不要忘记编译器不会去验证 Contract。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,529评论 5 475
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,015评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,409评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,385评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,387评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,466评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,880评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,528评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,727评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,528评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,602评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,302评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,873评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,890评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,132评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,777评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,310评论 2 342