Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Contract,开发者和 Kotlin 编译器之间的契约

Contract,开发者和 Kotlin 编译器之间的契约

作者头像
技术小黑屋
发布于 2020-01-23 13:33:06
发布于 2020-01-23 13:33:06
5960
举报
文章被收录于专栏:技术小黑屋技术小黑屋

相比 Java,使用 Kotlin 编程的时候,我们和kotlin编译器的交互行为会更多一些,比如我们可以通过inline来控制字节码的输出结果,使用注解也可以修改编译输出的class文件。

这里介绍一个和kotlin编译器更加好玩的特性,contract。可以理解成中文里面的契约。

不够智能的 Kotlin 编译器

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不为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

@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 } }

关于上面代码的一些简单解释

  • contract 采用DSL方式声明
  • returns(true) implies ([email protected] is News) 代表如果方法返回(returns) true,表明(implies) [email protected] 是News实例,而不是News?的实例,即[email protected]为非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

@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 } }

return(null) implies

1 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 } }

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

@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实例,非null

returnsNotNull implies

1 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实例,非null

callsInPlace 原地调用

callsInPlace(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 有这样几个变量

  • AT_MOST_ONCE 做多调用一次
  • EXACTLY_ONCE 只调用一次
  • AT_LEAST_ONCE 最少执行一次
  • UNKNOWN (the default). 未知,默认值

应用Contract的问题

由于目前Contract还处于实验阶段,需要使用相关的注解来表明开发者明确这一特性(以后可能修改,并自愿承担相应的变动和后果)。

目前我们可以使用UseExperimentalExperimentalContracts两种注解,以下为具体的使用示例。

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项目

对于非 Android项目,会有另外一个非注解的方式,那就是为模块增加编译选项。如下图。

当然,你也可以在模块的配置文件,增加-Xuse-experimental=kotlin.contracts.ExperimentalContractscompilerSettingsadditionalArguments中。

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

当方法行为与契约不符

  • 这种情况是可能且容易出现的,因为Contract并没有校验机制处理。
  • 当这种情况出现,就意味着我们向编译器提供了虚假的辅助信息
  • 一旦问题出现,对应的结果结果就是导致应用运行时崩溃。

比如下面的例子,我们的方法与契约不符

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

所以作为开发者,我们需要小心谨慎避免犯这种错误。

注意事项

  • Contract 自1.3才引入,而且是实验性的功能,未来的实现方式可能会有变动
  • Contract 目前只适用于top-level的方法,否则将会编译失败

Contract 如今还是实验功能,用还是不用

  • 是的,正如前面提到的Contract属于实验阶段,后期的规划,可能是作为正式功能引入还是变更实施方案,还是相对未知的。
  • 但是仅以个人的观点来看,还是推荐使用的。因为我觉得有些技术不需要等到稳定或者正式阶段就可以应用。

References

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
《Kotlin Contract 契约》极简教程
我们都知道Kotlin中有个非常nice的功能就是类型智能推导(官方称为smart cast), 不知道小伙伴们在使用Kotlin开发的过程中有没有遇到过这样的场景,会发现有时候智能推导能够正确识别出来,有时候却失败了。
一个会写诗的程序员
2019/08/09
1.5K0
《Kotlin Contract 契约》极简教程
Kotlin Contract
Kotlin 的智能推断是其语言的一大特色。 智能推断,能够根据类型检测自动转换类型。 但是,智能推断并没有想象中的强大,例如下面的代码就无法进行推断,导致编译失败: fun String?.i
fengzhizi715
2020/10/30
8030
Kotlin Contract
看不懂Kotlin源码?从Contracts 函数说起~
最近有朋友反馈说因为源码是Kotlin,所以看不懂。其实,很多时候看不懂Kotlin的源码很有可能是因为你不知道某些特定语法。正如你看不懂源码其实是因为不了解设计模式一样~
黄林晴
2022/06/12
7170
看不懂Kotlin源码?从Contracts 函数说起~
Kotlin Contracts DSL
从 Kotlin 1.2 版本开始,如果你查看 apply、 let 等函数的源码,你会发现比 1.1 版本多了几行不明觉厉的代码:
bennyhuo
2020/02/20
1.1K0
Kotlin---标准扩展函数
除了自定义扩展之外,Kotlin中也定义了很多的扩展函数,而这些扩展函数的接收类型是范型,也就是所有对象都可以使用。这些标准的扩展函数都放在了Standard.kt中。
None_Ling
2018/12/24
5550
Kotlin StandardKt
==TODO== 往往出现在子类实现抽象父类时被重写的抽象方法内,如果方法不重写就必须将 TODO 去除,否则会抛出异常
萬物並作吾以觀復
2021/11/24
3930
Kotlin 如何优雅地使用 Scope Functions
作用域函数:它是 Kotlin 标准库的函数,其唯一目的是在对象的上下文中执行代码块。 当您在提供了 lambda 表达式的对象上调用此类函数时,它会形成一个临时范围。 在此范围内,您可以在不使用其名称的情况下访问该对象。
fengzhizi715
2019/07/15
9760
Kotlin入门教程,快使用Kotlin吧
数组用Array类实现,和Java不同的地方在于,Array类有一个size属性表示数组长度,还有get和set方法,但是也可以使用array[position]的方式获取
用户2802329
2018/08/07
1.1K0
Kotlin入门教程,快使用Kotlin吧
使用Kotlin高效地开发Android App(二)总结
继上一篇文章介绍了项目中所使用的Kotlin特性,本文继续整理当前项目所用到的特性。
fengzhizi715
2018/08/24
6930
使用Kotlin高效地开发Android App(二)总结
Kotlin 中级篇(八):高阶函数详解与标准的高阶函数使用
所以这个函数的作用是:把字符串中的每一个字符转换为Int的值,用于累加,最后返回累加的值
玖柒的小窝
2021/12/06
9270
Kotlin 中级篇(八):高阶函数详解与标准的高阶函数使用
【Kotlin】标准库函数总结 ( apply 函数 | let 函数 | run 函数 | with 函数 | also 函数 | takeIf 函数 | takeUnless 函数 )
Kotlin 语言中 , 在 Standard.kt 源码中 , 为所有类型定义了一批标准库函数 , 所有的 Kotlin 类型都可以调用这些函数 ;
韩曙亮
2023/03/30
3.1K0
【Kotlin】标准库函数总结 ( apply 函数 | let 函数 | run 函数 | with 函数 | also 函数 | takeIf 函数 | takeUnless 函数 )
Kotlin 中的 run、let、with、apply、also、takeIf、takeUnless 语法糖使用和原理分析
在 Kotlin 有一些可以简化代码的语法糖,比如 run、let、with、apply、also、takeIf、takeUnless 等。
音视频开发进阶
2019/07/26
2.5K0
Kotlin 高级编程语言特性代码实例
fun <Ext, R> execute(domain: DomainEnum, biz: BizEnum, clz: Class<Ext>, f: (Ext) -> R): R
一个会写诗的程序员
2021/03/22
1.3K0
12. Kotlin 作用域函数(scope function)
学习 Kotlin 一定绕不开 run/let/apply/also 这四兄弟,它们是 Kotlin 使用频率最高的扩展方法(扩展方法在之前文章有介绍),它们也被称为作用域函数(scope functions)。今天我们就来了解一下它们。本文依然是按代码比较,字节码分析,和扩展思考三个方面进行分析。
sickworm
2020/04/26
1.1K0
【Kotlin】标准库函数 ④ ( takeIf 标准库函数 | takeUnless 标准库函数 )
Kotlin 语言中 , 在 Standard.kt 源码中 , 为所有类型定义了一批标准库函数 , 所有的 Kotlin 类型都可以调用这些函数 ;
韩曙亮
2023/03/30
1.5K0
【Kotlin】标准库函数 ④ ( takeIf 标准库函数 | takeUnless 标准库函数 )
使用Kotlin高效地开发Android App(三)
Kotlin基于Java的空指针提出了一个空安全的概念,即每个属性默认不可为null。
fengzhizi715
2018/08/24
9360
使用Kotlin高效地开发Android App(三)
【Kotlin】标准库函数 ① ( apply 标准库函数 | let 标准库函数 )
Kotlin 语言中 , 在 Standard.kt 源码中 , 为所有类型定义了一批标准库函数 , 所有的 Kotlin 类型都可以调用这些函数 ;
韩曙亮
2023/03/30
1.1K0
【Kotlin】标准库函数 ① ( apply 标准库函数 | let 标准库函数 )
Kotlin 基础 | 拒绝语法噪音
程序员最头痛的事莫过于看不懂别人的代码。缘由是各式各样的,但归结于一点就是复杂度太高。Kotlin 在降低代码复杂度上下了大功夫,运用一系列新的语法特性降低语法噪音,以求更简单直白地表达语义。
Rouse
2021/07/08
1.2K0
Kotlin 基础 | 拒绝语法噪音
为 Kotlin 项目设置编译选项
经常用终端的人都知道,终端命令有很多选项可以指定,这里我们以相关的kotlinc为例,我们可以在终端这样指定选项
技术小黑屋
2020/01/21
2.3K0
Kotlin 2.0 跟随全新的更快、更灵活的 K2 编译器一起发布
JetBrains 发布了 Kotlin 2.0 以及全新的 K2 编译器。虽然该语言本身没有引入新的语法,但 K2 编译器带来了一些优势,包括更快的构建、具有智能强制类型转换的扩展语言功能,以及开箱即用的多平台支持。
深度学习与Python
2024/06/17
2820
Kotlin 2.0 跟随全新的更快、更灵活的 K2 编译器一起发布
推荐阅读
相关推荐
《Kotlin Contract 契约》极简教程
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档