相比 Java,使用 Kotlin 编程的时候,我们和kotlin编译器的交互行为会更多一些,比如我们可以通过inline
来控制字节码的输出结果,使用注解也可以修改编译输出的class文件。
这里介绍一个和kotlin编译器更加好玩的特性,contract。可以理解成中文里面的契约。
Kotlin编译器向来是比较智能的,比如做类型推断和smart cast
等。但是有些时候,显得不是那么智能,比如下面的这段代码
1 2 3 4 5 6 7 8 9 10 11 12 13 | data class News(val publisherId: Int, val title: String) //检查标题是否合法,如果title为null或者内容为空返回false fun News?.isTitleValid(): Boolean { return this != null && title.isNotEmpty() } fun testNewsTitleValid(news: News?) { if (news.isTitleValid()) { news.title //编译失败 并报错 //Only safe (?.) or non-null asserted (!!.) calls //are allowed on a nullable receiver of type News? } } |
---|
上面的代码会让我们觉得Kotlin编译器很不智能,甚至是有些笨拙。
news.isTitleValid()
返回true,我们可以推测出news.title
不为null,也能推断出news不为nullnews.title
会导致编译报错 Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type News?
news?.title
或者是news!!.title
,但无论哪一种都不够优雅所以不争的结论就是,Kotlin编译器在if
语句内部无法推断news
是非null的。
可能有人会想,我觉得挺简单的啊,应该可以推断出来吧。
是的,如果仅仅以例子中如此简单的实现,大家都会觉得可以推断出来
但是
所以,不能推断也是有对应的考虑的。
所以我们面临的现实情况是
News?.isTitleValid
返回true,代表News实例不为null于是,开发者和编译器之间可以建立一个这样的契约
News?.isTitleValid
返回true,代表News实例不为null为例News?.isTitleValid
为true后,按照开发者预期,转换成非空的News实例,让开发者可以直接调用而 Kotlin 从1.3版本引入了Contract(契约),用来解决我们刚刚提到的问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | @ExperimentalContracts fun News?.isTitleValid(): Boolean { //contract 开始 contract { returns(true) implies (this@isTitleValid is News) } //contract 结束 return this != null && title.isNotEmpty() } @ExperimentalContracts fun testNewsTitleValid(news: News?) { if (news?.isTitleValid() == true) { news.title } } |
---|
关于上面代码的一些简单解释
returns(true) implies (
[email protected]
is News)
代表如果方法返回(returns) true,表明(implies) [email protected]
是News实例,而不是News?的实例,即[email protected]
为非null@ExperimentalContracts
(后面章节会提到)上面的契约为returns(true) implies
,除此之外,还有
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | @ExperimentalContracts fun News?.isFake(): Boolean { contract { returns(false) implies (this@isFake is News) } return this == null || this.publisherId == 1980 } @ExperimentalContracts fun testNewsIsFake(news: News?) { if (news.isFake()) { news?.title } else { news.title } } |
---|
News?.isFake
返回false,则表明[email protected]
是News
实例,非null1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | @ExperimentalContracts fun News?.copy(): Any? { contract { returns(null) implies (this@copy is News) } return if (this == null) { "EMPTY" } else { null } } @ExperimentalContracts fun testNewsCopy(news: News?) { if (news.copy() == null) { news.title } else { news?.title } } |
---|
News?.copy
返回null时,[email protected]
是News
实例,非null1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | @ExperimentalContracts fun News?.validate() { contract { returns() implies (this@validate is News) } if (this == null) { throw IllegalStateException("null instance") } if (publisherId < 0) { throw IllegalStateException("publisherId is less than 0") } if (title.isEmpty()) { throw IllegalStateException("title is empty") } } @ExperimentalContracts fun testNewsValidate(news: News?) { news.validate() news.title } |
---|
News?.validate()
顺利执行完毕,不抛出异常,则[email protected]
是News
实例,非null1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | @ExperimentalContracts fun News?.getTitleHashCode(): Int? { contract { returnsNotNull() implies (this@getTitleHashCode is News) } return this?.title?.hashCode() } @ExperimentalContracts fun testNewsGetTitleHashCode(news: News?) { if (news.getTitleHashCode() != null) { news.title } else { news?.title } } |
---|
News?.getTitleHashCode()
返回为非null,则[email protected]
是News
实例,非nullcallsInPlace(lambda, kind)和之前的契约不同,它让我们有能力告知编译器,lambda在什么时候,什么地方,以及执行次数等信息。
同样,我们继续看这样一段代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | package com.example.androidcontractsample fun getAppVersion() { val appVersion: Int safeRun { appVersion = 50 } } //安全运行runFunction,捕获异常 inline fun safeRun(runFunction: () -> Unit) { try { runFunction.invoke() } catch(t: Throwable) { t.printStackTrace() } } |
---|
当我们执行编译的时候,会得到这样的错误信息Captured values initialization is forbidden due to possible reassignment
因为上面的代码,也存在这里开发者知道一些信息,而编译器不知道的情况
runFunction
实参是否会执行runFunction
实参是否只执行一次还是多次(val赋值多次会出错)runFunction
实参执行时,是否getappVersion已经执行完毕runFunction
没有执行,appVersion
处于未初始化状态runFunction
执行多次,appVersion
被多次赋值,对于val是禁止的。1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | import kotlin.contracts.ExperimentalContracts import kotlin.contracts.InvocationKind import kotlin.contracts.contract @ExperimentalContracts fun getAppVersion() { val appVersion: Int safeRun { appVersion = 50 } } @ExperimentalContracts fun safeRun(runFunction: () -> Unit) { contract { //使用EXACTLY_ONCE callsInPlace(runFunction, InvocationKind.EXACTLY_ONCE) } try { runFunction() } catch (t: Throwable) { t.printStackTrace() } } |
---|
通过契约上面的代码实现了
safeRun
会在getAppVersion
执行的过程中执行,不会等到getAppVersion
执行完毕后执行safeRun
会确保runFunction
只会执行一次,不会多次执行注意:官方说使用callsInPlace作用的方法必须inline(A function declaring the callsInPlace effect must be inline.)。但是经过验证不inline也没有问题,只是对应的实现方式不同。
除此之外,上面提到的InvocationKind 有这样几个变量
由于目前Contract还处于实验阶段,需要使用相关的注解来表明开发者明确这一特性(以后可能修改,并自愿承担相应的变动和后果)。
目前我们可以使用UseExperimental
和ExperimentalContracts
两种注解,以下为具体的使用示例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | @UseExperimental(ExperimentalContracts::class) fun String?.isOK(): Boolean { contract { returns(true) implies(this@isOK is String) } return this != null && this.isNotEmpty() } @ExperimentalContracts fun String?.isGood(): Boolean { contract { returns(true) implies(this@isGood is String) } return this != null && this.isNotEmpty() } |
---|
对于非 Android项目,会有另外一个非注解的方式,那就是为模块增加编译选项。如下图。
当然,你也可以在模块的配置文件,增加-Xuse-experimental=kotlin.contracts.ExperimentalContracts
到compilerSettings
的additionalArguments
中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | <module type="JAVA_MODULE" version="4"> <component name="FacetManager"> <facet type="kotlin-language" name="Kotlin"> <configuration version="3" platform="JVM 1.8" useProjectSettings="false"> <compilerSettings> <option name="additionalArguments" value="-version -Xuse-experimental=kotlin.contracts.ExperimentalContracts" /> </compilerSettings> <compilerArguments> <option name="jvmTarget" value="1.8" /> <option name="languageVersion" value="1.3" /> <option name="apiVersion" value="1.3" /> </compilerArguments> </configuration> </facet> </component> <component name="NewModuleRootManager" inherit-compiler-output="true"> <exclude-output /> <content url="file://$MODULE_DIR$"> <sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" /> </content> <orderEntry type="inheritedJdk" /> <orderEntry type="sourceFolder" forTests="false" /> <orderEntry type="library" name="KotlinJavaRuntime" level="project" /> </component> </module |
---|
比如下面的例子,我们的方法与契约不符
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | @ExperimentalContracts fun validateByMistake(news: News?): Boolean { contract { returns(true) implies (news is News) } return true } @ExperimentalContracts fun testValidateByMistake(news: News?) { if (validateByMistake(news)) { news.title } } |
---|
当然随之而来的就是运行时的崩溃
1 2 3 4 5 6 7 8 | java.lang.NullPointerException: Attempt to invoke virtual method 'java.lang.String com.example.androidcontractsample.News.getTitle()' on a null object reference at com.example.androidcontractsample.NewsKt.testValidateByMistake(News.kt:91) at com.example.androidcontractsample.MainActivity.onCreate(MainActivity.kt:13) at android.app.Activity.performCreate(Activity.java:7698) at android.app.Activity.performCreate(Activity.java:7687) at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1299) at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3096) ... 11 more |
---|
所以作为开发者,我们需要小心谨慎避免犯这种错误。