Kotlin契约(Contract)

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

不难解释,通常函数的调用都是在运行时知道结果的,上述的notNullrunFun天然也是如此,函数调用的结果没法做为调用处编译时的上下文,即函数内部在编译时在调用处是不可见的,所以编译器没法经过这个上下文做出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用于提供callsInPlacereturns等函数的调用
其中上面的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标准库中的函数好比letcheckNotNull正常发挥做用,可是在你使用的时候,可能会有一些编译时的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的调用)告知给编译器,使得开发者能够把这些行为封装到函数中,同时还能发挥编译器的智能推导效果

相关文章
相关标签/搜索