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?
不难解释,一般函数的调用都是在运行时知道结果的,上述的notNull
和runFun
自然也是如此,函数调用的结果无法作为调用处编译时的上下文,即函数内部在编译时在调用处是不可见的,因此编译器无法通过这个上下文作出smart cast的行为
因此不要太难为编译器,我们应该给编译器一点提示,契约正式出场!
使用契约
runFun
的契约版本
先来解释一下这段代码含义
我们在runFun
的开头加入了contract
函数
其接受带一个无参无返回值的函数,而且这个函数还有一个值接收者ContractBuilder
用于提供callsInPlace
、returns
等函数的调用
其中上面的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标准库中的函数比如let
,checkNotNull
正常发挥作用,但是在你使用的时候,可能会有一些编译时的bug,而且将来API的使用可能会发生变动,所以请谨慎使用。
使用契约的好处
其实契约的好处并不是体现在开发者如何去使用它,因为标准库已经提供了利用契约实现的各种函数,满足了开发者的日常需求
分析契约 参考链接
Effect.kt
,里面定义了几个直接和间接继承于Effect
的接口,代码量不多,具体含义全部都写了出来
各个主要接口之间的关系
ContractBuilder.kt
中包含了使用契约时主要用到的函数,接口等
契约使用需要注意的地方
目前契约在使用时有以下限制
- 契约目前在kt标准库中大量被使用,但是不建议开发者使用,目前面向开发者的契约还有很多bug
- 我们只能在顶层函数体内使用契约,即我们不能在成员和类函数上使用它们。
contract
调用声明必须是函数体内第一条语句- 编译器无条件地信任契约,这意味着程序员负责编写正确合理的契约,
不要欺骗编译器它会伤心的。
总结
契约的作用就是把函数行为(比如例子中的null-check,和对action的调用)告知给编译器,使得开发者可以把这些行为封装到函数中,同时还能发挥编译器的智能推导效果