Kotlin Contracts DSL

Kotlin2018-12-18 08:43:18

从 Kotlin 1.2 版本开始,如果你查看 applylet 等函数的源码,你会发现比 1.1 版本多了几行不明觉厉的代码:

  1. public inline fun <T, R> T.let(block: (T) -> R): R {

  2.   // kotlin 1.2 加了下面三行代码

  3.   contract {

  4.      callsInPlace(block, InvocationKind.EXACTLY_ONCE)

  5.   }

  6.   // kotlin 1.2 加了上面三行代码

  7.   return block(this)

  8. }

很好,接下来就讲讲那几行多出来的代码到底有什么用。本文使用的 Kotlin 版本为 1.2.31。

简单的需求

假设我们有这样一段代码:

  1. fun some() {

  2.   var text: String? = getText()

  3.   if(text.isNullOrEmpty()) {

  4.      text = "我永远喜欢燕结芽"

  5.   }

  6.   println(text.length) // error, cannot smart cast to String

  7. }

稍有常识的人都会看出,如果我们的代码继续执行,这个可空类型的 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 内联展开:

  1. if(text == null || text.isEmpty()) {

  2.   text = "我永远喜欢燕结芽"

  3. }

  4. println(text.length) // ok, smart cast to String

为了解决这个问题,于是就有了 Contracts DSL。

Contracts DSL

Contracts DSL 可以为编译器提供关于函数行为的附加信息,帮助编译器分析函数的实际运行情况。

我们可以查看一下 isNullOrEmpty 的源码:

  1. public inline fun CharSequence?.isNullOrEmpty(): Boolean {

  2.   contract {

  3.      returns(false) implies (this@isNullOrEmpty != null)

  4.   }

  5.   return this == null || this.length == 0

  6. }

你可以看到 contract 代码块里面的那行代码,表示「如果返回值为 false,那么 this(函数的接收者)不为 null」。

因为这个东西目前还是个实验性特性,处于内部评估的状态,尚未对外公开发布,所以是默认关闭的。如果启用了该特性,那么编译器就能解析获取 Contracts DSL 所表达的信息,用于数据流分析。

为了开启这个特性,我们需要给编译器传入提供额外的编译参数: -Xeffect-system-Xread-deserialized-contracts。然后下面的代码就能够正常通过编译:

  1. fun test() {

  2.   val str: String?

  3.   run {

  4.      // captured value initialization is forbidden due to possible reassignment

  5.      str = "でないと、私のすごいとこ 見せられないじゃん"

  6.   }

  7.   println(str) // str not initialized

  8.   val notNull1: Any? = str

  9.   requireNotNull(notNull1)

  10.   println(notNull1.hashCode()) // cannot smart cast to Any

  11.   val notNull2: String? = str

  12.   if (!notNull2.isNullOrEmpty()) {

  13.      println(notNull2.length) // cannot smart cast to String

  14.   }

  15. }

虽然在 IDEA 里这些代码仍然会被标上红色下划线表示有错,但是加上编译器参数后的确能通过编译,也能够正常运行。

就拿上面例子的 run 函数说起,看看源码:

  1. public inline fun <R> run(block: () -> R): R {

  2.   contract {

  3.      callsInPlace(block, InvocationKind.EXACTLY_ONCE)

  4.   }

  5.   return block()

  6. }

编译器可以知道「传入的 lambda 会立即在“原地”执行有且仅有一次」,那么 str 一定会被初始化,而且不会被重新赋值。编译通过!

现在 Contracts DSL 位于 kotlin.internal.contracts 这个包内,是 internal 的,一般用户还无法直接拿来写自己的 contract,等JB那帮人把这个功能做好了就会公开这套API了。

编写自己的 contract

既然这玩意是 internal 的,那我把它改成 public 总能用了吧。

于是手工编译了一份魔改过的 Kotlin 标准库,使用后发现 IDEA 也能正确提示报错了。(貌似还需要加上编译器参数 -Xallow-kotlin-package(允许使用 kotlin 开头的包名))

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

实际体验的话,那个 implies() 目前只支持几个基本的模式(空检验、类型检验等,以后应该会增加新的模式),IDEA 的报错也是时好时坏(一切以编译结果为准)。

而且我尝试写了如下的 contract:

  1. inline fun <reified T> Any?.isInstanceOf(): Boolean {

  2.   contract {

  3.      returns(true) implies (this@isInstanceOf is T)

  4.   }

  5.   return this is T

  6. }

也不知道是我太鶸还是 Kotlin 太辣鸡,上面这个 contract 看起来不起作用。

嘛反正是处于实验阶段的特性,也不强求什么,至少比没有强(

跑个题

JetBrains 注解库有个 @Contract 可以实现类似的功能。虽然这个功能是 IDEA 提供的,不是 javac 的功能,并不能阻止错误的代码通过编译,仅仅只是增强 IDEA 的 Java 代码分析能力。

  1. // KotlinMain.kt

  2. // 用kotlin写具体实现以干掉IDEA强大的Java代码分析功能

  3. fun isNullOrEmpty(cs: CharSequence?) = cs == null || cs.isEmpty()

  4. fun getNullableString(): String? = null

  1. import org.jetbrains.annotations.Contract;

  2. public class JavaMain {

  3.   @Contract("null->true") // 表示「输入 null 则一定返回 true」

  4.   private static boolean isNullOrEmpty(CharSequence cs) {

  5.      return KotlinMainKt.isNullOrEmpty(cs);

  6.   }

  7.   public static void main(String[] args) {

  8.      String str = KotlinMainKt.getNullableString();

  9.      if (isNullOrEmpty(str)) {

  10.         // warning:调用hashCode可能会产生NullPointerException(这个warning是本来就有的)

  11.         System.out.println(str.hashCode());

  12.      } else {

  13.         // 通过@Contract提供的信息可知else分支里str不为null,所以不会有warning

  14.         System.out.println(str.hashCode());

  15.      }

  16.      boolean b = isNullOrEmpty(null);

  17.      // 通过@Contract提供的信息可知`b`永远为null

  18.      // 所以会有 warning:condition `b` is always true

  19.      if (b) System.out.println("でないと、私のすごいとこ 見せられないじゃん");

  20.   }

  21. }

当然除此之外还有其他更高级的东西(不能再跑题了(逃


转载请注明出处:微信公众号 Kotlin


Copyright © 温县电话机虚拟社区@2017