Contract,开发者和 Kotlin 编译器之间的契约 - 技术小黑屋
source link: https://droidyue.com/blog/2019/08/25/kotlin-contract-between-developers-and-the-compiler/?
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
相比 Java,使用 Kotlin 编程的时候,我们和kotlin编译器的交互行为会更多一些,比如我们可以通过inline
来控制字节码的输出结果,使用注解也可以修改编译输出的class文件。
这里介绍一个和kotlin编译器更加好玩的特性,contract。可以理解成中文里面的契约。
不够智能的 Kotlin 编译器
Kotlin编译器向来是比较智能的,比如做类型推断和smart cast
等。但是有些时候,显得不是那么智能,比如下面的这段代码
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
上面的代码会让我们觉得Kotlin编译器很不智能,甚至是有些笨拙。
news.isTitleValid()
返回true,我们可以推测出news.title
不为null,也能推断出news不为null- 但是即使这样,我们使用
news.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的。
为什么 Kotlin编译器不能推断出来呢
可能有人会想,我觉得挺简单的啊,应该可以推断出来吧。
是的,如果仅仅以例子中如此简单的实现,大家都会觉得可以推断出来
- 现实中的实践代码往往会比上面的复杂,比如涉及到多个调用和更加复杂的方法体实现等等
- 纵使可以做到,编译器也需要花费资源和时间来分析上下文,这其中随着层级加深,资源消耗和编译耗时也会增加。
所以,不能推断也是有对应的考虑的。
契约是什么
所以我们面临的现实情况是
- 作为开发者,我们了解较多的情况,比如
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 |
|
关于上面代码的一些简单解释
- contract 采用DSL方式声明
returns(true) implies (this@isTitleValid is News)
代表如果方法返回(returns) true,表明(implies)this@isTitleValid
是News实例,而不是News?的实例,即this@isTitleValid
为非null- 声明使用Contract的方法和其被调用的方法都需要使用
@ExperimentalContracts
(后面章节会提到)
其他的契约实现
上面的契约为returns(true) implies
,除此之外,还有
- returns(false) implies
- returns(null) implies
- returns implies
- returnsNotNull implies
- callsInPlace
returns(false) implies
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
- 当方法
News?.isFake
返回false,则表明this@isFake
是News
实例,非null
return(null) implies
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
- 当方法
News?.copy
返回null时,this@copy
是News
实例,非null
returns implies
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
- 如果方法
News?.validate()
顺利执行完毕,不抛出异常,则this@validate
是News
实例,非null
returnsNotNull implies
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
- 如果
News?.getTitleHashCode()
返回为非null,则this@getTitleHashCode
是News
实例,非null
callsInPlace 原地调用
callsInPlace(lambda, kind)和之前的契约不同,它让我们有能力告知编译器,lambda在什么时候,什么地方,以及执行次数等信息。
同样,我们继续看这样一段代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
当我们执行编译的时候,会得到这样的错误信息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 |
|
通过契约上面的代码实现了
safeRun
会在getAppVersion
执行的过程中执行,不会等到getAppVersion
执行完毕后执行safeRun
会确保runFunction
只会执行一次,不会多次执行
注意:官方说使用callsInPlace作用的方法必须inline(A function declaring the callsInPlace effect must be inline.)。但是经过验证不inline也没有问题,只是对应的实现方式不同。
除此之外,上面提到的InvocationKind 有这样几个变量
- AT_MOST_ONCE 做多调用一次
- EXACTLY_ONCE 只调用一次
- AT_LEAST_ONCE 最少执行一次
- UNKNOWN (the default). 未知,默认值
应用Contract的问题
由于目前Contract还处于实验阶段,需要使用相关的注解来表明开发者明确这一特性(以后可能修改,并自愿承担相应的变动和后果)。
目前我们可以使用UseExperimental
和ExperimentalContracts
两种注解,以下为具体的使用示例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
非 Android项目
对于非 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 |
|
当方法行为与契约不符
- 这种情况是可能且容易出现的,因为Contract并没有校验机制处理。
- 当这种情况出现,就意味着我们向编译器提供了虚假的辅助信息
- 一旦问题出现,对应的结果结果就是导致应用运行时崩溃。
比如下面的例子,我们的方法与契约不符
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
当然随之而来的就是运行时的崩溃
1 2 3 4 5 6 7 8 |
|
所以作为开发者,我们需要小心谨慎避免犯这种错误。
- Contract 自1.3才引入,而且是实验性的功能,未来的实现方式可能会有变动
- Contract 目前只适用于top-level的方法,否则将会编译失败
Contract 如今还是实验功能,用还是不用
- 是的,正如前面提到的Contract属于实验阶段,后期的规划,可能是作为正式功能引入还是变更实施方案,还是相对未知的。
- 但是仅以个人的观点来看,还是推荐使用的。因为我觉得有些技术不需要等到稳定或者正式阶段就可以应用。
References
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK