看不懂Kotlin源码?从Contracts 函数说起~

2022-06-12 17:20:42 浏览数 (1)

前言

最近有朋友反馈说因为源码是Kotlin,所以看不懂。其实,很多时候看不懂Kotlin的源码很有可能是因为你不知道某些特定语法。正如你看不懂源码其实是因为不了解设计模式一样~

举个例子

以Kotlin中常用的isNullOrEmpty方法为例,源码如下所示:

代码语言:javascript复制
@kotlin.internal.InlineOnly
public inline fun CharSequence?.isNullOrEmpty(): Boolean {
    contract {
        returns(false) implies (this@isNullOrEmpty != null)
    }

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

咦?代码很简单,不过怎么看不懂呢?contract是什么鬼,implies 又是什么鬼? 其实当你了解contract函数的使用方法之后,类似的源码你就都能看懂了。

Contracts是什么?

Contracts是合同、契约的意思。从Kotlin1.3版本的时候就被引入了,简单的来说Contracts可以用来解决一些编译器无法完成的功能。

所以,它到底是干嘛的呢?

我们来定义一个类似的函数,用于检测某个对象是否为null,首先定义一个User对象,代码如下所示:

代码语言:javascript复制
data class User(val name: String, val age: Int)

再定义一个检查User对象是否为空的方法,代码如下所示:

代码语言:javascript复制
fun isEmpty(user: User?) {
    if (user == null) {
        throw IllegalArgumentException("is empty")
    }
}

然后我们在业务方法中调用,代码如下所示:

代码语言:javascript复制
fun work(user: User?){
    isEmpty(user = user)
    setText(user.name)
}

 此时这个方法是无法编译通过的,编译器会提醒你user是一个可为空的对象,需要添加"?.",

但是从我们的代码逻辑来看,我们首先调用了isEmpty方法,如果user对象为null的话,则在isEmpty方法中已经抛出了异常,也就是说不会走到setText方法中。换句话说,如果代码可以执行到setText这一行那么user对象肯定是不为空的。那么现在该如何处理呢?这就需要Contracts出场了。

Contracts的使用方法

Contracts API 一般如下所示:

代码语言:javascript复制
fun A() {
    contract {

    }
}

当前主要使用方式有returns、callsInPlace等,首先我们来看returns的使用场景。

Returns Contracts

代码语言:javascript复制
contract {
    returns($value) implies($condition)
}

returns用来告诉编辑器当返回值为value时 condition条件成立。当前value可以是布尔类型或者空类型,condition条件表达式也需要返回布尔类型。

比如,我们现在修改isEmpty方法,代码如下所示:

代码语言:javascript复制
@ExperimentalContracts
fun isEmpty(user: User?) {
    contract {
        returns() implies (user != null)
    }
    if (user == null) {
        throw IllegalArgumentException("is empty")
    }
}

这里我们通过contract约束告诉编译器,如果isEmpty函数可以正常返回的话,那么说明user不为空。换句话理解,也就说当user为空的时候由于抛出了异常,所以isEmpty函数是无法返回的。

你会发现,当修改了isEmpty方法之后,work方法已经不再报错了。

这是因为我们已经通过isEmpty方法告诉编译器,若代码可以执行到setText,说明user对象一定不为空。由于这个函数一直是实验性的API,所以这里要加上@ExperimentalContracts注解。

不过,目前Kotlin源码中已经很多用到了这个API,所以我们不用担心以后会发生大的变化。接着我们再来看CallInPlace的使用场景。

CallInPlace Contracts

CallInPlace的使用也是很广泛的,比如我们在Kotlin中常用的标准函数apply、also等。这里以apply函数为例,apply函数源码如下所示:

代码语言:javascript复制
@kotlin.internal.InlineOnly
public inline fun <T> T.apply(block: T.() -> Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block()
    return this
}

从源码中我们可以看到,里面使用到了callsInPlace方法。callsInPlace可以告诉编辑器block方法仅会执行一次。而不是0次或多次。当然也可以设置方法至少执行一次或最多执行一次。这个读者可自行尝试。

Contracts的源码

首先我们来看下contract函数的源码,如下所示:

代码语言:javascript复制
@ContractsDsl
@ExperimentalContracts
@InlineOnly
@SinceKotlin("1.3")
@Suppress("UNUSED_PARAMETER")
public inline fun contract(builder: ContractBuilder.() -> Unit) { }

contract是一个内联函数,需要一个ContractBuilder对象,接着来看ContractBuilder对象源码,如下所示:

代码语言:javascript复制
@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
}

我们可以看到returns方法返回了Returns,callsInPlace方法返回了CallsInPlace,而Returns对象是SimpleEffect的接口实现自接口Effect,CallsInPalce对象是Effect接口,源码如下所示:

代码语言:javascript复制
@ContractsDsl
@ExperimentalContracts
@SinceKotlin("1.3")
public interface SimpleEffect : Effect {

    @ContractsDsl
    @ExperimentalContracts
    public infix fun implies(booleanExpression: Boolean): ConditionalEffect
}

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

从SimpleEffect源码中可以看出,它实现了一个中缀函数名字为 implies,来告诉编译器booleanExpression将成立,以此来和编译器约定好。

写在最后

contract其实是通过与编译器达成约定的方式,弥补了智能转化的短板。

近期可能会有些许事情发生,借用“秉心说”大佬今天说的一句话就是 —— 2022年,好事多磨吧~

0 人点赞