现在我们用kotlin开发的过程中,经常使用kotlin的一些标准库函数,如let、apply等,点击去看到这些库函数的实现时,会发现他们有一个共同的特点
@kotlin.internal.InlineOnly
public inline fun <T, R> T.let(block: (T) -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block(this)
}
@kotlin.internal.InlineOnly
public inline fun <T> T.apply(block: T.() -> Unit): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block()
return this
}
都有一个contract 的闭包调用,这个是什么呢?其实这个翻译过来就是"契约",在kotlin中就代表着一种约定:告诉编译器,方法传递过来的参数肯定会被执行。
Contract功能
kotlin有一个很实用的功能——智能类型转换,如下代码所示:
open class Car(var wheels: Int = 4)
class Benz : Car() {
fun run() {}
}
fun startWork(car: Car) {
if (car is Benz) {
car.run()
}
}
fun startWork(car: Car) {
if (isBenZ(car)) {
car.run() //IDE提示找不到这个方法
}
}
我们在if语句里,判断了car的类型之后,就可以直接调用run方法,但是如果把这个if判断语句抽离到一个方法中,智能类型转换就不生效了,在编译时,会提示找不到方法。那么使用contract,就可以消除这个错误提示:
@ExperimentalContracts
fun isBenz(car: Car): Boolean {
contract {
returns(true) implies (car is Benz)
}
return car is Benz
}
因为是contract目前依然处于实验阶段,所以要加@ExperimentalContracts注解。
Contract原理
/**
* 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源码,contract是指定函数的契约,contract是一个内联方法,参数builder是ContractBuilder的一个拓展方法,ContractBuilder是一个接口,里面有四个方法
/**
* Provides a scope, where the functions of the contract DSL, such as [returns], [callsInPlace], etc.,
* can be used to describe the contract of a function.
*
* This type is used as a receiver type of the lambda function passed to the [contract] function.
*
* @see contract
*/
@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和ReturnsNotNull一类是以返回值为导向
- CallsInPlace是以执行次数为导向
以返回值为导向
以第一类Returns方法来看,之前的源码,returns(true) implies (car is Benz)是运行在ContractBuilder中的DSL语句
@ExperimentalContracts
fun isBenz(car: Car): Boolean {
contract {
returns(true) implies (car is Benz)
}
return car is Benz
}
returns继承于SimpleEffect,SimpleEffect继承于Effect
/**
* 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
}
Effect代表着的是一个contract所在方法运行后对编译器的影响,而通过中缀表达式implies是保证booleanExpression表达式的成立。
returns(true) implies (car is Benz)
所以这句代码就是告诉编译器contract所在的方法返回true时,代表着car is Benz表达式,在之后的编译中,编译器会遵循着这个契约,这就就弥补了智能类型转换结果在方法中也不可以向下传递的短板。
以执行次数为导向
这类contract就是告诉编译器,传入的block()代码肯定会被执行,以apply函数源码为例
@kotlin.internal.InlineOnly
public inline fun <T, R> T.let(block: (T) -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block(this)
}
调用的CallsInPlace方法,有两个参数,第一个标定了传入的lambda表达式,第二个则是lambda执行的次数,有四种类型
@ContractsDsl
@ExperimentalContracts
@SinceKotlin("1.3")
public enum class InvocationKind {
@ContractsDsl AT_MOST_ONCE, //最多一次
@ContractsDsl AT_LEAST_ONCE, //至少一次
@ContractsDsl EXACTLY_ONCE, //就执行一次
@ContractsDsl UNKNOWN //不确定
}
举个例子
fun test() {
var str: String
changeContent {
str = "xx"
}
str.length
}
fun changeContent(block: () -> Unit) {
block()
}
这里运行会报错,因为编译器不知道changeContent()方法中的block()是否被执行,也就不知道str是否被初始化过,这时可以加一个Contract
@ExperimentalContracts
fun changeContent(block: () -> Unit) {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block()
}
编译器就会知道changeContent方法中的block()肯定被执行了一次。
总结
虽然Contract为开发者解决了编译器不够智能的问题,可以使代码更简练,但是这个智能的做法依然是通过开发者告诉编译器的,编译器无条件地遵守这个契约,这也就为开发者提出了额外的要求,那就是一定要确保 contract 的正确性,不然将会导致很多不可控制的错误,甚至是崩溃,所以目前Contract还处于kotlin官方的实验中,期待它“转正”的那天。