注册

Kotlin浅析之Contract

在进行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是什么和怎么用的部分场景,还有更多的场景以及具体的原理有兴趣的同学可以深挖~


感谢大家的观看!!!


作者:苏打水08
链接:https://juejin.cn/post/7102300475243888647
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

0 个评论

要回复文章请先登录注册