前言
最近有朋友反馈说因为源码是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年,好事多磨吧~