Kotlin - 契约特性(Contract)


Kotlin契约

目录
  • Kotlin契约
    • 配置环境
    • 为何要使用契约
    • 使用契约
    • 使用契约的好处
    • 分析契约 参考链接
    • 契约使用需要注意的地方
    • 总结

Contract是Kotlin1.3的东西,比较新,目前还是处于实现性阶段(Experimental),即API在稳定版之前可能会发生变动。由于是实现性API,使用时需要额外添加注解,下面代码中会具体讲到。

配置环境

在project的gradle文件中

由于契约处于实验性

可以通过添加以下编译器选项(可选),这样就不用在使用契约时处处添加注解了

在Module的gradle文件中

为何要使用契约

先看一下下面这段简单的代码

getValue中调用一次runFun,运行时效果相当于把ret = 15调用了一次,注意是运行时,在编译时编译器并不知道runFun调用时传入的action有无被调用,因而编译时报错Variable 'ret' must be initialized

再看一个类似的例子

当字符串不为null时则将长度打印出来。

It works fine.

但如果程序中对字符串有很多这种判断,应该就会想到这个判断写成一个函数,减少代码冗余。于是就可能写成下面的版本

这个版本对可空字符串的检查封装成了拓展函数形式,一眼望上去,聪明的编译器应该会在s.length的地方,有一个smart cast,将String?自动转换成String以使得length能正确被调用,但事实却是:编译器报错
Only safe (?.) or non-null asserted(!!.) calls are allowed on a nullable reciever of type String?,编译器并没有做上述类型转换,Why?

不难解释,一般函数的调用都是在运行时知道结果的,上述的notNullrunFun自然也是如此,函数调用的结果无法作为调用处编译时的上下文,即函数内部在编译时在调用处是不可见的,因此编译器无法通过这个上下文作出smart cast的行为

因此不要太难为编译器,我们应该给编译器一点提示,契约正式出场!

使用契约

runFun 的契约版本

先来解释一下这段代码含义

我们在runFun的开头加入了contract函数

其接受带一个无参无返回值的函数,而且这个函数还有一个值接收者ContractBuilder用于提供callsInPlacereturns等函数的调用

其中上面的callsInPlace两个参数,第一个是任意函数类型,第二个参数表示传入的函数会被调用的次数,比如例子中的InvocationKind.EXACTLY_ONCE表明函数在运行时会被执行一次。说到这里,大概可以猜到,contract面向编译器的,给编译器看的,就是为了向编译器表明调用contract函数的这个函数(比如上面的runFun)是做什么的,getValue中调用契约版的runFun函数,编译器就能知道,传入的action函数会被调用一次,即变量ret将会在运行时会被初始化成15。

(除了InvocationKind.EXACTLY_ONCE外还有AT_LEAST_ONCE等常量,具体含义查阅文档)

notNull 的契约版本

contract 代码表明当implies后的值成立,函数将会返回returns函数中的内容,注意这里implies是一个中缀运算符

所以notNull函数中的contract告诉了编译器,当字符串不为null时函数在运行时将会返回true

契约能让编译器smart cast的能力进一步发挥出来,这也说明了你可以"欺骗"编译器,比如在刚才的notNull函数中,将returns中的true改成false(自己体会),而且再次说明契约在开发环境中为实验性API,这表明它即使能在kotlin标准库中的函数比如letcheckNotNull正常发挥作用,但是在你使用的时候,可能会有一些编译时的bug,而且将来API的使用可能会发生变动,所以请谨慎使用

使用契约的好处

其实契约的好处并不是体现在开发者如何去使用它,因为标准库已经提供了利用契约实现的各种函数,满足了开发者的日常需求

分析契约 参考链接

Effect.kt ,里面定义了几个直接和间接继承于Effect的接口,代码量不多,具体含义全部都写了出来

各个主要接口之间的关系

ContractBuilder.kt 中包含了使用契约时主要用到的函数,接口等

契约使用需要注意的地方

目前契约在使用时有以下限制

  1. 契约目前在kt标准库中大量被使用,但是不建议开发者使用,目前面向开发者的契约还有很多bug
  2. 我们只能在顶层函数体内使用契约,即我们不能在成员和类函数上使用它们。
  3. contract调用声明必须是函数体内第一条语句
  4. 编译器无条件地信任契约,这意味着程序员负责编写正确合理的契约,不要欺骗编译器它会伤心的

总结

契约的作用就是把函数行为(比如例子中的null-check,和对action的调用)告知给编译器,使得开发者可以把这些行为封装到函数中,同时还能发挥编译器的智能推导效果