Contract是Kotlin1.3的东西,比较新,目前仍是处于实现性阶段(Experimental),即API在稳定版以前可能会发生变更。因为是实现性API,使用时须要额外添加注解,下面代码中会具体讲到。
android
在项目的gradle文件中程序员
buildscript {
dependencies {
//kotlin_version确保在1.3或以上
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
复制代码
因为契约处于实验性
能够经过添加如下编译器选项(可选),这样就不用在使用契约时到处添加注解了
在模块的gradle文件中函数
android {
kotlinOptions {
freeCompilerArgs += [
"-Xuse-experimental=kotlin.contracts.ExperimentalContracts"
]
}
}
复制代码
先看一下下面这段简单的代码post
fun runFun(action: () -> Unit) {
action()
}
fun getValue(): Int {
var ret: Int
runFun {
ret = 15;
}
return ret
}
复制代码
在getValue
中调用一次runFun,运行时效果至关于把ret = 15
调用了一次,注意是运行时,在编译时编译器并不知道runFun
调用时传入的action有无被调用,于是编译时报错Variable 'ret' must be initialized
gradle
再看一个相似的例子ui
fun printLength(s: String?) {
if (s != null) {
Log.d("TAG", "${s.length}")
}
}
复制代码
当字符串不为null
时则将长度打印出来。
this
It works fine.
spa
但若是程序中对字符串有不少这种判断,应该就会想到这个判断写成一个函数,减小代码冗余。因而就可能写成下面的版本code
fun printLength(s: String?) {
if (s.notNull()) {
Log.d("TAG", "${s.length}")
}
}
fun String?.notNull(): Boolean {
return this != null
}
复制代码
这个版本对可空字符串的检查封装成了拓展函数形式,一眼望上去,聪明的编译器应该会在s.length
的地方,有一个smart cast,将String?
自动转换成String
以使得length能正确被调用,但事实倒是:编译器报错 Only safe (?.) or non-null asserted(!!.) calls are allowed on a nullable reciever of type String?
,编译器并无作上述类型转换,Why?
cdn
不难解释,通常函数的调用都是在运行时知道结果的,上述的notNull
和runFun
天然也是如此,函数调用的结果没法做为调用处编译时的上下文,即函数内部在编译时在调用处是不可见的,所以编译器没法经过这个上下文做出smart cast的行为
所以不要太难为编译器,咱们应该给编译器一点提示,契约正式出场!
runFun
的契约版本
//有了上面模块gradle配置,注解可省略,若是两个都没有,编译器报错
@ExperimentalContracts
fun runFun(action: () -> Unit) {
contract {
callsInPlace(action, kotlin.contracts.InvocationKind.EXACTLY_ONCE)
}
action()
}
复制代码
先来解释一下这段代码含义
咱们在runFun
的开头加入了contract
函数
//contract源码
public inline fun contract(builder: ContractBuilder.() -> Unit) { }
复制代码
其接受带一个无参无返回值的函数,并且这个函数还有一个值接收者ContractBuilder
用于提供callsInPlace
、returns
等函数的调用
其中上面的callsInPlace
两个参数,第一个是任意函数类型,第二个参数表示传入的函数会被调用的次数,好比例子中的InvocationKind.EXACTLY_ONCE
代表函数在运行时会被执行一次。说到这里,大概能够猜到,contract
是面向编译器的,给编译器看的,就是为了向编译器代表调用contract
函数的这个函数(好比上面的runFun
)是作什么的,getValue
中调用契约版的runFun
函数,编译器就能知道,传入的action
函数会被调用一次,即变量ret
将会在运行时会被初始化成15。
(除了InvocationKind.EXACTLY_ONCE
外还有AT_LEAST_ONCE
等常量,具体含义查阅文档)
notNull
的契约版本
@ExperimentalContracts
fun String?.notNull(): Boolean {
contract {
returns(true) implies (this@notNull != null)
}
return this != null
}
复制代码
contract
代码代表当implies
后的值成立,函数将会返回returns
函数中的内容,注意这里implies
是一个中缀运算符
因此notNull
函数中的contract
告诉了编译器,当字符串不为null
时函数在运行时将会返回true
契约能让编译器smart cast的能力进一步发挥出来,这也说明了你能够"欺骗"编译器,好比在刚才的notNull
函数中,将returns
中的true
改为false
(本身体会),并且再次说明契约在开发环境中为实验性API,这代表它即便能在kotlin标准库中的函数好比let
,checkNotNull
正常发挥做用,可是在你使用的时候,可能会有一些编译时的bug,并且未来API的使用可能会发生变更,因此请谨慎使用。
Effect.kt
,里面定义了几个直接和间接继承于Effect
的接口,代码量很少,具体含义所有都写了出来
//用来表示一个函数被调用的效果
public interface Effect
//继承Effect接口,用来表示在观察函数调用后另外一个效果以后,某些条件的效果为true。
public interface ConditionalEffect : Effect
//继承Effect接口,用来表示一个函数调用后的结果(这个通常就是最为普通的Effect)
public interface SimpleEffect : Effect {
public infix fun implies(booleanExpression: Boolean): ConditionalEffect //infix代表了implies函数是一个中缀函数,那么它调用起来就像是中缀表达式同样
}
//继承SimpleEffect接口,用来表示当一个函数正常返回给定的返回值
public interface Returns : SimpleEffect
//继承SimpleEffect接口,用来表示当一个函数正常返回非空的返回值
public interface ReturnsNotNull : SimpleEffect
//继承Effect接口,用来表示调用函数式参数(lambda表达式参数)的效果,而且函数式参数(lambda表达式参数)只能在本身函数被调用期间被调用,当本身函数被调用结束后,函数式参数(lambda表达式参数)不能被执行.
public interface CallsInPlace : Effect
复制代码
各个主要接口之间的关系
ContractBuilder.kt
中包含了使用契约时主要用到的函数,接口等
目前契约在使用时有如下限制
contract
调用声明必须是函数体内第一条语句契约的做用就是把函数行为(好比例子中的null-check,和对action的调用)告知给编译器,使得开发者能够把这些行为封装到函数中,同时还能发挥编译器的智能推导效果