5

Kotlin Contracts DSL | 萌夜雀的人头会社

 2 years ago
source link: https://aisia.moe/2018/03/25/kotlin-contracts-dsl/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

从 Kotlin 1.2 版本开始,如果你查看 applylet 等函数的源码,你会发现比 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

然后随便写了一下,看起来就像这个截图这样:

201803300824.jpg

实际体验的话,那个 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/


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK