Kotlin Contracts是kotlin 1.3推出的一个新特性,它主要作用是进一步提升kotlin编译器的智能转换(smartcasts)能力,先看看下面这个示例。
fun foo(s: String?) {
if (s != null) {
println("length of '$s' is ${s.length}") //编译器自动将s转换为String类型
}
}
这样很符合逻辑,其实这背后是kotlin编译器帮我们做了智能转换,但是并不是所有的情况编译器都能这样默默的处理了,比如说,我们把判空处理放在另外一个独立的方法中。
1fun main() {
2 foo("hello kotlin")
3}
4
5fun isNotNull(s: String?) = s != null
6
7fun foo(s: String?) {
8 if (isNotNull(s)) {
9 println("length of '$s' is ${s.length}") //编译出错 Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type String?
10 }
11}
仅仅是把功能封装成了一个单独的方法,之前的智能转换功能居然不生效了,这对于开发者来说很容易推断出s
可以转换为String类型而不是String?,但是对于编译器来说却比较麻烦,为了增强这类场景kotlin编译器的智能转换能力,所以推出了Contracts机制。
我们将以上代码判断字符串的部分改为kotlin标准库中的扩展方法isNullOrBlank
,则可以顺利编译通过。
fun main() {
foo("hello kotlin")
}
fun foo(s: String?) {
if (!s.isNullOrBlank()) {
println("length of '$s' is ${s.length}")
}
}
下面是isNullOrBlank
方法的定义,从定义中我们可以看出它使用了Contracts机制。
public inline fun CharSequence?.isNullOrBlank(): Boolean {
contract {
returns(false) implies (this@isNullOrBlank != null)
}
return this == null || this.isBlank()
}
后面是正常的判空处理,重点是contract这一段代码,它代表合约的内容:如果方法返回true,表明字符串实例不是null。编译器可以根据这个约定来决定是否可以将字符串s实例从String?类型转换为String类型。我们将上面提到编译出错的代码改动一下,使用Contracts机制来帮助编译器完成智能转换。
1import kotlin.contracts.ExperimentalContracts
2import kotlin.contracts.contract
3
4@ExperimentalContracts
5fun main() {
6 foo("hello kotlin")
7}
8
9@ExperimentalContracts
10fun isNotNull(s: String?): Boolean {
11 contract {
12 returns(true) implies (s != null)
13 }
14 return s != null
15}
16
17@ExperimentalContracts
18fun foo(s: String?) {
19 if (isNotNull(s)) {
20 println("length of '$s' is ${s.length}")
21 }
22}
除了returns(value) implies(condition)
这种形式,还有以下几种。
returns() implies(condition)
,如果方法调用正常返回,则表明condition成立。returnsNotNull() implies(condition)
,如果方法返回值不为null,则表明condition成立。除了能够帮助编译器智能转换以外,Contracts机制还能帮助编译器在存在高阶函数时,判断一个变量是否初始化。先看看下面示例,因为编译器无法判断变量是否会初始化,所以导致编译出错。
1fun main() {
2 foo()
3}
4
5fun bar(block: () -> Unit) {
6 block()
7}
8
9fun foo() {
10 var x: Int
11 bar {
12 x = 42
13 }
14 println(x)
15}
从开发者角度来看,这个x = 42
这行代码会执行一次,也就是说x
会在println方法之前赋值,但是对于编译器来说,它却不能很智能的知道这个情况,所以就出现了编译错误。在ContractBuilder中还定义了方法callsInPlace,用于提示编译器传入高阶函数中的表达式的执行情况。
比如说上面的代码,如果我们使用Contracts机制提示编译器,block会被执行一次,那么它就能够知道x变量会在println方法之前被赋值,也就不会出现编译错误了。
1import kotlin.contracts.ExperimentalContracts
2import kotlin.contracts.InvocationKind
3import kotlin.contracts.contract
4
5@ExperimentalContracts
6fun main() {
7 foo()
8}
9
10@ExperimentalContracts
11fun bar(block: () -> Unit) {
12 contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) }
13 block()
14}
15
16@ExperimentalContracts
17fun foo() {
18 var x: Int
19 bar {
20 x = 42
21 }
22 println(x)
23}