Kotlin浅析之Contract

1,143 阅读4分钟

在进行kotlin的项目开发中,我们依赖kotlin语法糖相比java可以更高效地产出,kotlin的彩蛋众多,这篇文章着重跟大家聊一聊Contract,其实Contract在官方函数中其实也有被多次使用,比如我们常用的let、apply、also、isNullOrEmpty等函数:

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

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

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

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

接下来,我们来了解一下contract到底是什么以及怎么用?

一、Contract是什么?

contract翻译过来意思是"契约",那么既然是"契约",约定的双方又是谁?

“我”和"你",心连心,同住地球村?搞叉了,再来!

契约的双方实际上是"开发者'和"编译器" ,我们都知道,kotlin编译器有着智能推断自动类型转换的功能。但实际上,它的智能推断有时候并不那么智能,下面会讲到,而官方为开发者预留了一个通道去与编译器沟通,这就是contract存在的意义。

二、Contact怎么用?

首先,我们定义一个String常规的判空扩展函数

/**
 * 字符串扩展函数判空,常规方式
 * @receiver String?  接收类型
 * @return Boolean    是否为空
 */
fun String?.isNullOrEmptyWithoutContract(): Boolean {
    return this == null || this.isEmpty()
}

然后,我们来调用看看

/**
 * 问题示例1 使用自定义函数判空,编译器无感知
 * @param name String? 传入的姓名字符串
 */
private fun problemNull(name: String?) {
    // 用常规方式的自定义扩展函数对局部变量判空
    if (!name.isNullOrEmptyWithoutContract()) {
        //name.length报错,自定义扩展函数中的判空逻辑未同步到编译器 Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type String?
        Log.d(TAG, "name:$name,length:${name.length}")
    }
}

结果,在函数内部调用外部自定义字符串判空函数,不起作用,这是因为编译器并不知道这种间接的判空是不是有效的,而这时候,我们请出contract来表演看看:

判空扩展函数contract returns改造

/**
 * 字符串扩展函数判空,contract方式
 * @receiver String?  接收类型
 * @return Boolean    是否为空
 */
@ExperimentalContracts
fun String?.isNullOrEmptyWithContract(): Boolean {
    contract {
        returns(false) implies (this@isNullOrEmptyWithContract != null)
    }
    return this == null || this.isEmpty()
}

/**
 * 解决问题1 自定义函数判空后结果同步编译器
 * @param name String? 传入的姓名字符串
 */
@ExperimentalContracts
fun fixProblemNull(name: String?) {
    // 用contract方式的自定义扩展函数对局部变量判空
    if (!name.isNullOrEmptyWithContract()) {
        //运行正常
        Log.d(TAG, "name:$name,length:${name.length}")
    }
}

可以看到,判空扩展函数加入了contract之后,编译器就懂事了,但编译器是如何懂事的呢?contract内部到底跟编译器说了什么悄悄话?咱们先分析下判空扩展函数的代码

contract {
    returns(false) implies (this@isNullOrEmptyWithContract != null)
}

contract所包裹的语句,实际上就是我们要告诉编译器的逻辑,这里的returns(false) 代表当前函数isNullOrEmptyWithContract()的返回值也就是 return this == null || this.isEmpty()如果是false,那么会告知编译器implies后面的表达式也就是this@isNullOrEmptyWithContract != null成立,也就是调用者对象String不为空,那么后面在打印name.length的时候编译器就知道name不为空拉,这就是开发者与编译器的契约!

其次,我们发现除了resturns的用法外,常用的apply扩展函数里面的contract是callsInPlace形式,那么callsInPlace又是什么意思?

/**
 * 定义apply函数,常规方式
 * @receiver T  接收类型
 * @param block [@kotlin.ExtensionFunctionType] Function1<T, Unit> 函数入参
 * @return T   返回类型
 */
fun <T> T.applyWithoutContract(block: T.() -> Unit): T {
    block()
    return this
}

/**
 * 问题示例2 函数执行变量初始化,编译器无感知
 */
fun problemInit() {
    var name: String
    // 用常规方式的自定义扩展函数对局部变量赋值
    applyWithoutContract {
        // 编译器实际上不知道这个函数入参有没有被调用
        name = "WenChangJi"
    }
    // 报错 'Variable 'name' must be initialized'
    Log.d(TAG, "name:${name}")
}

这里我们给间接给局部变量name去赋值,但是后续使用时编译器报错声称name没有初始化,采取以往经验,我们加入contract去改造试试:

/**
 *
 * 定义apply函数,contract方式
 * @receiver T  接收类型
 * @param block [@kotlin.ExtensionFunctionType] Function1<T, Unit> 函数入参
 * @return T   返回类型
 */
@ExperimentalContracts
fun <T> T.applyWithContract(block: T.() -> Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block()
    return this
}

/**
 * 解决问题2 函数执行变量初始化后同步编译器
 */
@ExperimentalContracts
fun fixProblemInit() {
    var name: String
    // 用contract方式的自定义扩展函数对局部变量赋值
    applyWithContract {
        // applyWithContract内部契约告知编译器,这里绝对会调用一次的,也就一定会初始化
        name = "WenChangJi"
    }
    // 运行正常
    Log.d(TAG, "name:${name}")

}

这里我们并没有采用returns告知编译器在满足什么条件下什么表达式成立,而是采用callsInPlace方式告知编译器入参函数block的调用规则,callsInPlace(block, InvocationKind.EXACTLY_ONCE)即是告诉编译器block在内部会被调用一次,也就是后续调用时的语句name = "WenChangJi"会被调用一次进行赋值,那么在使用name时编译器就不会说没有初始化之类的问题拉!

callsInPlace内部次数的常量值由以下几种:

常量值含义
- InvocationKind.AT_MOST_ONCE最多调用一次
InvocationKind.AT_LEAST_ONCE最少调用一次
InvocationKind.EXACTLY_ONCE调用一次
InvocationKind.UNKNOWN未知

最后,咱们这边文章只是讲解了Contract是什么和怎么用的部分场景,还有更多的场景以及具体的原理有兴趣的同学可以深挖~

感谢大家的观看!!!