Kotlin Contracts DSL
从 Kotlin 1.2 版本开始,如果你查看 apply
、let
等函数的源码,你会发现比 1.1 版本多了几行不明觉厉的代码:
public inline fun <T, R> T.let(block: (T) -> R): R {
// kotlin 1.2 加了下面三行代码
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
// kotlin 1.2 加了上面三行代码
return block(this)
}
很好,接下来就讲讲那几行多出来的代码到底有什么用。
本文使用的 Kotlin 版本为 1.2.31。
简单的需求
假设我们有这样一段代码:
fun some() {
var text: String? = getText()
if(text.isNullOrEmpty()) {
text = "我永远喜欢燕结芽"
}
println(text.length) // error, cannot smart cast to String
}
稍有常识的人都会看出,如果我们的代码继续执行,这个可空类型的 text
变量,在最后一行那里不可能为 null
。
但是编译器傻乎乎地向你丢出了一个编译错误:Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type String?
原因在于编译器不能深入分析每个函数(在这个例子中是 isNullOrEmpty
)的数据流,无法得知「test
不为空」的事实,也就无法进行 Smart Cast 了。
所以如果要享受到 Smart Cast 的便利的话,可以手动将 isNullOrEmpty
内联展开:
if(text == null || text.isEmpty()) {
text = "我永远喜欢燕结芽"
}
println(text.length) // ok, smart cast to String
但是这很麻烦,而且还不好看。为了解决这个问题,于是就有了 Contracts DSL。
Contracts DSL
Contracts DSL 可以为编译器提供关于函数行为的附加信息,帮助编译器分析函数的实际运行情况,从而让更多正确的代码能通过编译(例如上面的例子)。
我们可以查看一下 isNullOrEmpty 的源码:
public inline fun CharSequence?.isNullOrEmpty(): Boolean {
contract {
returns(false) implies (this@isNullOrEmpty != null)
}
return this == null || this.length == 0
}
这里简单解释一下 contract
代码块里面的那行代码,表示「如果返回值为false
,那么this
(函数的接收者)不为null
」。
因为这个东西目前还是个实验性特性,处于内部评估的状态,尚未对外公开发布,所以是默认关闭的。如果启用了该特性,那么编译器就能解析获取 Contracts DSL 所表达的信息,用于数据流分析。
在 Kotlin 1.2 版本,为了开启这个特性,我们需要给编译器传入提供额外的编译参数:
-Xeffect-system
-Xread-deserialized-contracts
然后下面的代码就能够正常通过编译:
// 如果未开启 contract, 则会出现注释里的编译错误
fun test() {
val str: String?
run {
// captured value initialization is forbidden due to possible reassignment
str = "でないと、私のすごいとこ 見せられないじゃん"
}
println(str) // str not initialized
val notNull1: Any? = str
requireNotNull(notNull1)
println(notNull1.hashCode()) // cannot smart cast to Any
val notNull2: String? = str
if (!notNull2.isNullOrEmpty()) {
println(notNull2.length) // cannot smart cast to String
}
}
虽然在 IDEA 里这些代码仍然会被标上红色下划线表示有错,但是加上编译器参数后的确能通过编译,也能够正常运行。
就拿上面例子的 run
函数说起,看看源码:
public inline fun <R> run(block: () -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block()
}
编译器可以知道「传入的 lambda 会立即在“原地”执行有且仅有一次」,那么 str
一定会被初始化,而且不会被重新赋值。编译通过!
在 Kotlin 1.2 版本里 Contracts DSL 位于 kotlin.internal.contracts
这个包内,是 internal
的,一般用户还无法直接拿来写自己的 contract。
这个特性已经在 Kotlin 1.3 版本实装。
试着编写自己的 contract
在 1.2 版本编写自定义 contract 的方法:
将 1.2 版本标准库里的 contract 源码文件复制出来,丢到自己项目的源码文件夹里(也就是和自己的代码放在一起),包名保持 kotlin.internal.contracts
不要变,然后再加上编译器参数:
-Xeffect-system
-Xread-deserialized-contracts
-Xallow-kotlin-package
然后随便写了一下,看起来就像这个截图这样:
实际体验的话,那个 implies()
目前只支持几个基本的模式(空检验、类型检验等,以后应该会增加新的模式),IDEA 的报错也是时好时坏(一切以编译结果为准)。
而且我尝试写了如下的 contract:
inline fun <reified T> Any?.isInstanceOf(): Boolean {
contract {
returns(true) implies (this@isInstanceOf is T)
}
return this is T
}
也不知道是我太鶸还是 Kotlin 太辣鸡,上面这个 contract 看起来不起作用。
嘛反正是处于实验阶段的特性,也不强求什么,至少比没有强(
1.3 版本正式发布后,已经不需要搞七搞八,可以自由使用了。
2020/7/13更新:在 Kotlin 1.4-M1 版本中,上面的带有 reified 的泛型参数的 contract 已经可以使用了。
本作品采用知识共享 署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可,转载请注明出处。
本文链接:https://aisia.moe/2018/03/25/kotlin-contracts-dsl/