Kotlin contract 用法及原理
在阅读Kotlin源码的时候,会发现很多常用方法里都有contract的身影:
/**
* 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).
*/
public inline fun <T> T.apply(block: T.() -> Unit): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block()
return this
}
/**
* Calls the specified function [block] with `this` value as its argument and returns `this` value.
*
* For detailed usage information see the documentation for [scope functions](https://kotlinlang.org/docs/reference/scope-functions.html#also).
*/
public inline fun <T> T.also(block: (T) -> Unit): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block(this)
return this
}
contract(契约)是一种 Kotlin 面向编译器约定的一种规则,它帮助编译器更加智能地识别某些需要特定的代码条件,为代码创建更加友好的上下文关联环境。
1、contract定义
Kotlin在1.3版本以实验室功能的方式开始引入contract,截止至当前Kotlin最新版本1.6.10,contract方法依然添加有@ExperimentalContracts注解,这说明官方认为其能力还不够稳定。
/**
* 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) { }
在其构造类ContractBuilder中定义了四种方法:returns()、returns(value: Any?)、returnsNotNull()、callsInPlace(...),方法的作用后面举例子详细说明就会明白它的作用。
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,均继承自Effect接口:
/**
* Describes a situation when a function returns normally with a given return value.
*
* @see ContractBuilder.returns
*/
public interface Returns : SimpleEffect
/**
* Describes a situation when a function returns normally with any non-null return value.
*
* @see ContractBuilder.returnsNotNull
*/
public interface ReturnsNotNull : SimpleEffect
/**
* An effect of calling a functional parameter in place.
*
* A function is said to call its functional parameter in place, if the functional parameter is only invoked
* while the execution has not been returned from the function, and the functional parameter cannot be
* invoked after the function is completed.
*
* @see ContractBuilder.callsInPlace
*/
public interface CallsInPlace : Effect
Returns和 ReturnsNotNull代表的是返回结果的效果导向、CallsInPlace则代表的是执行次数的效果导向。
2、使用场景
为什么需要contract呢?它的作用是什么,继续往下看。声明一个抽象接口File:
abstract class File
子类 PdfFile继承自 File,并且定义了一个专属的子类方法。
class PdfFile: File() {
fun convertToWord() {
// do
}
}
在Kotlin中,有自动类型推断,一但判断过类型,后面会自动转换成对应类型使用。这个叫做智能类型转换:Smart cast to PdfFile。
fun move(file: File) {
if (file is PdfFile) {
//Smart cast to PdfFile
file.convertToWord()
}
}
但是如果通过方法判断的呢?判断逻辑抽取到独立的方法中:
fun isPDF(file: File): Boolean {
//逻辑可能更复杂
return file is PdfFile
}
由于没有直接上下文,这时候编译器就不能帮我们自动类型转换,会报错:Unresolved reference: convertToWord。
fun move2(file: File) {
if (isPDF(file)) {
//Unresolved reference: convertToWord
file.convertToWord()
}
}
虽然编译器完全可以在编译的时候深入方法分析,进而实现类型推断,但是会浪费编译时间,完全没必要。这时候contract契约就出现,通过人为的约定,告诉编译器一些信息。人是会出错的,所以这种方式对开发水平有要求,contract定义的契约一定要可靠。
3、使用contract
这种场景下contract就派上用场了,可以让参数类型在执行的方法上下文传递,方法执行完毕后,继续将类型等信息传递给后续的方法进行编译。
3.1、根据返回结果约定
借助returns()、returns(value: Any?)、returnsNotNull()可以根据方法执行的结果返回约定信息。
@OptIn(ExperimentalContracts::class)
fun isPDF(file: File): Boolean {
contract {
returns(true) implies (file is PdfFile)
}
return file is PdfFile
}
比如在前面的例子里,通过加入contract中的Returns效果,此时在调用 isPDF(file) 返回 true 后,编译器已经知道参数file就是 PdfFile类型,达到了类型转换在抽离到独立方法后,参数类型依然可以向下传递的作用,让代码更加地趋向了自然语言,提高代码的可读性和编码的便利性。
同时需要给调用的方法上也加上注解@ExperimentalContracts,这时候又可以愉快的进行自动类型转换了:Smart cast to PdfFile。
@ExperimentalContracts
fun move2(file: File) {
if (isPDF(file)) {
//Smart cast to PdfFile
file.convertToWord()
}
}
看一下字节码对应的java代码,其实就是编译器根据contract提供的约定条件进行强制类型转换:
@ExperimentalContracts
public static final void move2(@NotNull File file) {
Intrinsics.checkNotNullParameter(file, "file");
if (isPDF(file)) {
//编译器根据contract提供的约定条件进行强制类型转换
((PdfFile)file).convertToWord();
}
}
3.2、根据执行次数约定
常用的kotlin方法apply中就使用了contract中的callsInPlace约定,标识该lambda只会执行一次。
@kotlin.internal.InlineOnly
public inline fun <T> T.apply(block: T.() -> Unit): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block()
return this
}
编译器通过该 contract 得以知道扩展方法 block 的被执行次数的情况。
/**
* Specifies that the function parameter [lambda] is invoked in place.
*
* This contract specifies that:
* 1. the function [lambda] can only be invoked during the call of the owner function,
* and it won't be invoked after that owner function call is completed;
* 2. _(optionally)_ the function [lambda] is invoked the number of times specified by the [kind] parameter,
* see the [InvocationKind] enum for possible values.
*
* A function declaring the `callsInPlace` effect must be _inline_.
*
*/
@ContractsDsl public fun <R> callsInPlace(lambda: Function<R>, kind: InvocationKind = InvocationKind.UNKNOWN): CallsInPlace
InvocationKind有四种类:AT_MOST_ONCE、AT_LEAST_ONCE、EXACTLY_ONCE、UNKNOWN。
public enum class InvocationKind {
/**
* 最多执行一次
*/
@ContractsDsl AT_MOST_ONCE,
/**
* 至少执行一次
*/
@ContractsDsl AT_LEAST_ONCE,
/**
* 只执行一次
*/
@ContractsDsl EXACTLY_ONCE,
/**
* 未知、默认值
*/
@ContractsDsl UNKNOWN
}
同样的再举个例子,方便理解callsInPlace的效果。定义了一个方法,接受一个lambda表达式作为参数
fun init(block: () -> Unit) {
block()
}
当借助该方法初始化值时,编译器会报错,因为后续用到了这个值,编译器不知道有没有初始化:Variable 'data' must be initialized。
fun test() {
var data: String
//初始化放到lambda中执行
init {
data = ""
}
//编译器无法知道是否成功执行初始化
data.length
}
这时候就需要用到约定,告诉编译器,该lambda至少会执行一次,相应的变量一定会初始化。
@ExperimentalContracts
fun init(block: () -> Unit) {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block()
}
给调用的方法也加上@ExperimentalContracts注解,编译器就知道block初始化快会执行一次这个约定,后续再访问data变量就知道已经初始化过了。
@ExperimentalContracts
fun test() {
var data: String
init {
data = ""
}
data.length
}
由于是人为的约定,编译器也会无条件相信并执行这些约定。所以对开发的水平有要求,不能马虎,约定的场景不能存在漏洞,否则执行过程中会出现异常。