什么是 contract
contract(契约)是一种 Kotlin 面向编译器约定的一种规则,它帮助编译器更加智能地识别某些需要特定的代码条件,为代码创建更加友好的上下文关联环境。
Kotlin 在 1.3 版本以实验室功能的方式开始引入 contract, 截止至当前 Kotlin 最新版本 1.6.10,contract 方法依然添加有 @ExperimentalContracts 注解。
这说明官方认为其能力还不够稳定,但从 Kotlin 标准库已经随处可以看见 contract 的情况下看,contract 应该很快就会变为 release feature,我们可以先了解一下 contract 的能力和用法。
常见的 contract 调用
Standart.kt
@kotlin.internal.InlineOnly
public inline fun <T> T.apply(block: T.() -> Unit): T {
contract { // 告诉编译器,这个 block 一定会执行一次
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block()
return this
}
@kotlin.internal.InlineOnly
public inline fun <T, R> T.let(block: (T) -> R): R {
contract { // 告诉编译器,这个 block 一定会执行一次
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block(this)
}
引入 contract 的原因
Kotlin 编译器很强大,从它可以编译成 java 字节码和 JavaScript 就可以看出。但是再强大的编译器也有他的局限性(严格意义上甚至说不上是局限性,只是一种当前最优方案的折中做法),其中就包括智能类型转换和空判断,先举个例子:
// 声明一个抽象父类
abstract class Animal
// 子类 Bird 继承自 Animal
class Bird : Animal() {
fun fly() {
// do fly
}
}
在测试类里面进行智能类型转换
class Tester {
fun move(animal: Animal) {
if (animal is Bird) {
animal.fly() // 由于前面已经做过类型判断,所以这里可以进行类型的智能转换
}
}
}
但是如果把这个智能类型转换抽到一个方法中,这种智能转换就无法关联到代码上下文中
class Tester {
private fun isBird(animal: Animal) = animal is Bird
fun move(animal: Animal) {
if (isBird(animal)) { // 即使这里返回 true ,编译器无法传递当前 animal 是 Bird 类型
animal.fly() // 这里会报编译错误,无法找到 fly() 方法
}
}
}
在一个看似很简单的判断中,编译器就在一个方法中就已经无法传递 animal 的类型。当然这个只是对于我们调用者来说,编译器难道就做不到吗?答案是 可以但没必要,这是因为
第一,函数的执行结果无法直接给到编译时使用。再者,以 isBird(animal) 为例,即使编译时可以传递 animal 的具体类型,但是如果 isBird(animal) 嵌套多层调用,且调用的函数实现复杂的话,这样编译器就需要花费更多的资源去分析上下文并传递类型,这会造成了编译时长和资源占用的增加
问题是调用者已经明明可以很轻易的知道在这个上下文中,类型已经是可以被指定的,编译器却无法识别。contract 来了,它就是为了解决这种问题而诞生的,有了它,我们就可以在编译阶段 显式地 告诉编译器在特定条件下
得到我们期望的上下文数据,而不需要编译器再自行去做代码检查
contract 使用
在上面的例子中,我们就可以通过 contract 调用来让 animal 是 Bird 这个类型在单独的方法判断后可以继续向下传递
class Tester {
@ExperimentalContracts // 定义 contract 处需要加上实验性质的注解
private fun isBird(animal: Animal): Boolean {
contract { // 定义一个 contract,表示如果 isBird 这个方法返回 true,那么意味着 animal 就是 Bird 类型
returns(true) implies (animal is Bird)
}
return animal is Bird
}
@ExperimentalContracts // 调用含有 contract 的方法也需要加入实验性质的注解
fun move(animal: Animal) {
if (isBird(animal)) {
animal.fly() // isBird 返回 true,这里已经让编译器知道 animal 就是 Bird 类型
}
}
}
通过加入 contract 后,此时在调用 isBird(animal) 返回 true 后,编译器已经知道 animal 就是 Bird 类型,达到了只能类型转换在抽离到独立方法后,依然可以向下传递的作用,这样可以让代码更加地趋向了自然语言,提高了代码的可读性。
而事实上,contract 本身就是 DSL 方式声明的
而通过转换 Kotlin 代码成 java 发现,在加入 contract 后,kotlin 是将 animal 强转成了 Bird 类型
public final class Tester {
@ExperimentalContracts
private final boolean isBird(Animal animal) {
return animal instanceof Bird;
}
@ExperimentalContracts
public final void move(@NotNull Animal animal) {
Intrinsics.checkNotNullParameter(animal, "animal");
if (this.isBird(animal)) {
((Bird)animal).fly(); // 加入 contract 后,kotlin 编译器在此处将 animal 强转为了 Bird
}
}
}
contract 原理
先来看下 contract 的定义
ContractBuilder.kt
/**
* Specifies the contract of a function.
*
* The contract description must be at the beginning of a function and have at least one effect.
*
* Only the top-level functions can have a contract for now.
*
* @param builder the lambda where the contract of a function is described with the help of the [ContractBuilder] members.
*
*/
@ContractsDsl
@ExperimentalContracts
@InlineOnly
@SinceKotlin("1.3")
@Suppress("UNUSED_PARAMETER")
public inline fun contract(builder: ContractBuilder.() -> Unit) { }
contract 是一个内联方法,参数是一个 ContractBuilder 的扩展函数,ContractBuilder 暴露了 4 个接口,分别是
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
}
4 个接口返回的类型都为 kotlin.contracts.Effect 的子类,主要可以分为两大部分:
- Returns 和 ReturnsNotNull 代表的是返回结果的效果导向 contract
- CallsInPlace 则代表的是执行次数的效果导向 contract
返回结果的效果导向 contract
在前面的 demo 中的是返回结果导向的例子,回头再看下代码
@ExperimentalContracts // 定义 contract 处需要加上实验性质的注解
private fun isBird(animal: Animal): Boolean {
contract { // 定义一个 contract,表示如果 isBird 这个方法返回 true,那么意味着 animal 就是 Bird 类型
returns(true) implies (animal is Bird)
}
return animal is Bird
}
从 returns(true) implies (animal is Bird) 很轻易地看出 contract 的 DSL 声明方式。往下看 returns(true) 的实现
@ContractsDsl public fun returns(value: Any?): Returns
返回一个 Returns 对象,Returns 继承自 SimpleEffect,SimpleEffect 继承自 Effect。看下 SimpleEffect 的定义
/**
* Represents an effect of a function invocation,
* either directly observable, such as the function returning normally,
* or a side-effect, such as the function's lambda parameter being called in place.
*
* The inheritors are used in [ContractBuilder] to describe the contract of a function.
*
* @see ConditionalEffect
* @see SimpleEffect
* @see CallsInPlace
*/
@ContractsDsl
@ExperimentalContracts
@SinceKotlin("1.3")
public interface Effect
/**
* An effect that can be observed after a function invocation.
*
* @see ContractBuilder.returns
* @see ContractBuilder.returnsNotNull
*/
@ContractsDsl
@ExperimentalContracts
@SinceKotlin("1.3")
public interface SimpleEffect : Effect {
/**
* Specifies that this effect, when observed, guarantees [booleanExpression] to be true.
*
* Note: [booleanExpression] can accept only a subset of boolean expressions,
* where a function parameter or receiver (`this`) undergoes
* - true of false checks, in case if the parameter or receiver is `Boolean`;
* - null-checks (`== null`, `!= null`);
* - instance-checks (`is`, `!is`);
* - a combination of the above with the help of logic operators (`&&`, `||`, `!`).
*/
@ContractsDsl
@ExperimentalContracts
public infix fun implies(booleanExpression: Boolean): ConditionalEffect
}
它代表一个 contract 所在方法执行后对编译器的影响,而通过中缀方法 implies 告诉编译器 booleanExpression 表达式将成立,再一次看前面 demo 的 contract 调用
returns(true) implies (animal is Bird)
这段代码会告诉编译器,当调用 contract 所在这个方法返回 true 时意味着 animal 这个对象就是 Bird 类型,在往后的编译中,编译器将遵守这个约定。通过这样,就弥补了智能类型转换结果在方法中也不可以向下传递的短板
执行次数的效果导向 contract
这种 contract 在 Kotlin 标准库中是最常见的 contract,我们以 apply 函数为例
/**
* Calls the specified function [block] with `this` value as its receiver and returns `this` value.
*
* For detailed usage information see the documentation for [scope functions](https://kotlinlang.org/docs/reference/scope-functions.html#apply).
*/
@kotlin.internal.InlineOnly
public inline fun <T> T.apply(block: T.() -> Unit): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block()
return this
}
先解释下这里 contract 的作用:它将表明 block 这个扩展方法一定会在这里被执行一次
@ContractsDsl public fun <R> callsInPlace(lambda: Function<R>, kind: InvocationKind = InvocationKind.UNKNOWN): CallsInPlace
callsInPlace 同样作为 ContractBuilder 的成员方法,参数为两个,分别是:
- lambda: 需要标记会执行的 block
- kind: 上面 block 将会被执行的次数,kind 有四种类型
public enum class InvocationKind {
@ContractsDsl AT_MOST_ONCE, // 至多一次
@ContractsDsl AT_LEAST_ONCE, // 至少一次
@ContractsDsl EXACTLY_ONCE, // 有且只有一次
@ContractsDsl UNKNOWN // 未知,默认值
}
callsInPlace 的返回值为 CallsInPlace,同样也是继承自 Effect。编译器通过该 contract 得以知道扩展方法 block 的被执行情况。举个简单的例子:
class Tester {
@ExperimentalContracts
fun test() {
var strObject: String
initStr {
strObject = "This field must be initialized"
}
strObject.length // 编译错误,编译器不知道 strObject 是否有被初始化
}
@ExperimentalContracts
fun initStr(block: () -> Unit) {
block()
}
}
上面的方法将编译错误,报错信息如下
Variable 'strObject' must be initialized
那是因为编译器不知道 initStr 方法中的 block 有没有被执行,也即是不知道 strObject 有没有被初始化。这时可以加个 contract
@ExperimentalContracts
fun initStr(block: () -> Unit) {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block()
}
这样就顺利编译通过,因为此时,编译器已经被 contract 告知,initStr 方法中的 block 一定会被执行一次
总结
contract 为开发者解决了编译器不够智能的问题,这样可以使代码更简练,更加通俗易懂。但是这个智能的做法是通过开发者主观代码告诉编译器的,编译器无条件地遵守这个约定,这也就为开发者提出了
额外的要求,那就是一定要确保 contract 的正确性,不然将会导致很多不可控制的错误,甚至是崩溃。
还是以最开始的 demo 为例子,虽然不是特别恰当,当仍然说明了上面所说的风险:
class Tester {
private fun isBird(animal: Animal) = animal is Bird
fun move(animal: Animal) {
if (isBird(animal)) {
animal.fly()
} else { // 由于 contract 的错误逻辑,将导致下面代码的崩溃
animal.swin()
}
}
}
我们假设当前传入的 animal 的实际对象是 Dog,而此时,如果在 isBird() 方法中 contract 错误,就有可能导致崩溃。
@ExperimentalContracts //
private fun isBird(animal: Animal): Boolean {
contract { // 注意看这里,为了试验,写了一个低级的错误,当 return false 时,代表当前的 animal 为是 Fish
returns(false) implies (animal is Finish)
}
return animal is Bird
}
导致 move 方法中走到 else 分支,而当前 Dog 是没有 swin 方法的,这样就崩溃了