Kotlin 的标准库提供了不少方便的实用工具函数,比如 with, let, apply 之流,这些工具函数有一个共同特征:都调用了 contract() 函数

@kotlin.internal.InlineOnly
public inline fun <T, R> with(receiver: T, block: T.() -> R): R {contract {callsInPlace(block, InvocationKind.EXACTLY_ONCE)}return receiver.block()
}@kotlin.internal.InlineOnly
public inline fun repeat(times: Int, action: (Int) -> Unit) {contract { callsInPlace(action) }for (index in 0 until times) {action(index)}
}

contract?协议?它到底是起什么作用?

函数协议

contract 其实就是一个顶层函数,所以可以称之为函数协议,因为它就是用于函数约定的协议

@ContractsDsl
@ExperimentalContracts
@InlineOnly
@SinceKotlin("1.3")
@Suppress("UNUSED_PARAMETER")
public inline fun contract(builder: ContractBuilder.() -> Unit) { }

用法上,它有两点要求:

  • 仅用于顶层方法
  • 协议描述须置于方法开头,且至少包含一个「效应」(Effect)

可以看到,contract 的函数体为空,居然没有实现,真是一个神奇的存在。这么一来,此方法的关键点就只在于它的参数了。

ContractBuilder

contract的参数是一个将 ContractBuilder 作为接受者的lambda,而 ContractBuilder 是一个接口:

@ContractsDsl
@ExperimentalContracts
@SinceKotlin("1.3")
public interface ContractBuilder {@ContractsDsl public fun returns(): Returns@ContractsDsl public fun returns(value: Any?): Returns@ContractsDsl public fun returnsNotNull(): ReturnsNotNull@ContractsDsl public fun <R> callsInPlace(lambda: Function<R>, kind: InvocationKind = InvocationKind.UNKNOWN): CallsInPlace
}

其四个方法分别对应了四种协议类型,它们的功能如下:

  • returns:表明所在方法正常返回无异常
  • returns(value: Any?):表明所在方法正常执行,并返回 value(其值只能是 true、false 或者 null)
  • returnsNotNull():表明所在方法正常执行,且返回任意非 null 值
  • callsInPlace(lambda: Function<R>, kind: InvocationKind = InvocationKind.UNKNOWN):声明 lambada 只在所在方法内执行,所在方法执行完毕后,不会再被其他方法调用;可通过 kind 指定调用次数

前面已经说了,contract 的实现为空,所以作为接受着的 ContractBuilder 类型,根本没有实现类 —— 因为没有地方调用,就不需要啊。它的存在,只是为了声明所谓的协议代编译器使用。

InvocationKind

InvocationKind 是一个枚举类型,用于给 callsInPlace 协议方法指定执行次数的说明:

@ContractsDsl
@ExperimentalContracts
@SinceKotlin("1.3")
public enum class InvocationKind {// 函数参数执行一次或者不执行@ContractsDsl AT_MOST_ONCE,// 函数参数至少执行一次@ContractsDsl AT_LEAST_ONCE,// 函数参数执行一次@ContractsDsl EXACTLY_ONCE,// 函数参数执行次数未知@ContractsDsl UNKNOWN
}

InvocationKind.UNKNOWN,次数未知,其实就是指任意次数。标准工具函数中,repeat 就指定的此类型,因为其「重复」次数由参数传入,确实未知;而除它外,其余像 let、with 这些,都是用的InvocationKind.EXACTLY_ONCE,即单次执行。

Effect

Effect 接口类型,表示一个方法的执行协议约定,其不同子接口,对应不同的协议类型,前面提到的 Returns、ReturnsNotNull、CallsInPlace 均为它的子类型。

public interface Effectpublic interface ConditionalEffect : Effectpublic interface SimpleEffect : Effect {public infix fun implies(booleanExpression: Boolean): ConditionalEffect
}public interface Returns : SimpleEffectpublic interface ReturnsNotNull : SimpleEffectpublic interface CallsInPlace : Effect

简单明了,全员接口!来看一个官方使用,以便理解下这些接口的意义和使用:

public inline fun Array<*>?.isNullOrEmpty(): Boolean {contract {returns(false) implies (this@isNullOrEmpty != null)}return this == null || this.isEmpty()
}

这里涉及到两个 Effect:ReturnsConditionalEffect。此方法的功能为:判断数组为 null 或者是无元素空数组。它的 contract 约定是这样的:

  1. 调用 returns(value: Any?) 获得 Returns 协议(当然也就是 SimpleEffect 协议),其传入值是 false
  2. 第1步的 Returns 调用 implies 方法,条件是「本对象非空」,得到了一个 ConditionalEffect
  3. 于是,最终协议的意思是:函数返回 false 意味着 接受者对象非空

isNullOrEmpty() 的功能性代码给出了返回值为 true 的条件。虽然反过来说,不满足该条件,返回值就是 false,但还是通过 contract 协议里首先说明了这一点。

协议的意义

讲到这里,contract 协议涉及到的基本类型及其使用已经清楚了。回过头来,前面说到,contract() 的实现为空,即函数体为空,没有实际逻辑。这说明,这个调用是没有实际执行效果的,纯粹是为编译器服务。
不妨模仿着 let 写一个带自定义 contract 测试一下这个结论:

// 类比于ContractBuilder
interface Bonjour {// 协议方法fun <R> parler(f: Function<R>)  {println("parler something")}
}// 顶层协议声明工具,类比于contract
inline fun bonjour(b: Bonjour.() -> Unit) {}// 模仿let
fun<T, R> T.letForTest(block: (T) -> R): R {println("test before")bonjour {println("test in bonjour")parler<String> {""}}println("test after")return block(this)
}fun main(args: Array<String>) {"abc".letForTest {println("main: $it called")}
}

letForTest() 是类似于 let 的工具方法(其本身功能逻辑不重要)。执行结果:

test before
test after
main: abc called

如预期,bonjour 协议以及 Bonjour 协议构造器中的所有日志都未打印,都未执行。

这再一次印证,contract 协议仅为编译器提供信息。那协议对编码来说到底有什么意义呢?来看看下面的场景:

fun getString(): String? {TODO()
}fun String?.isAvailable(): Boolean {return this != null && this.length > 0
}

getString() 方法返回一个 String 类型,但是有可能为 null。isAvailable 是 String? 类型的扩展,用以判断是否一个字符串非空且长度大于 0。使用如下:

val target = getString()
if (target.isAvailable()) {val result: String = target
}

按代码的设计初衷,上述调用没问题,target.isAvailable() 为 true,证明 target 是非空且长度大于 0 的字符串,然后内部将它赋给 String 类型 —— 相当于 String? 转换成 String。

可惜,上述代码,编译器不认得,报错了:

Type mismatch.Required:StringFound:String?

编译器果然没你我聪明啊!要解决这个问题,自然就得今天的主角上场了:

fun String?.isAvailable(): Boolean {contract {returns(true) implies (this@isAvailable != null)}return this != null && this.length > 0
}

使用 contract 协议指定了一个 ConditionalEffect,描述意思为:如果函数返回true,意味着 Receiver 类型非空。然后,编译器终于懂了,前面的错误提示消失。

这就是协议的意义所在:让编译器看不懂的代码更加明确清晰。

小结

函数协议可以说是写工具类函数的利器,可以解决很多因为编译器不够智能而带来的尴尬问题。不过需要明白的是,函数协议还是实验性质的,还没有正式发布为 stable 功能,所以是有可能被 Kotlin 官方 去掉的。

作者:王可大虾
链接:https://juejin.cn/post/7128258776376803359
注:更多Android学习笔记+视频资料请扫描下方二维码在线领取~

Kotlin 标准库随处可见的 contract 到底是什么?相关推荐

  1. Kotlin 标准库中run、let、also、apply、with函数的用法和区别

    run 函数 定义: inline fun <R> run(block: () -> R): R //1 Calls the specified function block and ...

  2. Kotlin StandardKt 标准库源码走一波

    距离上篇Kotlin文章,应该有差不多半年时间.没别的原因,因为,懒,而且,想产出一篇稍微质量好的博客好难. 最近在研究Python,所以最近也可能会更新一些Python的学习笔记. Standard ...

  3. C/C++标准库到底是什么?

    C/C++ 标准库 在学习 C/C++ 的日子里,我们经常会有一个困惑:我们在代码里使用的标准库函数和类都是哪里来的?谁实现了它们?它们是打包好在操作系统里了吗?有没有官方的 C/C++ 手册呢? 在 ...

  4. C++:从C继承的标准库

    C++从C继承了的标准库 , 这就意味着  C 中 可以使用的标准库函数 在C++ 中都可以使用 , 但是需要注意的是 , 这些标准库函数在C++中不再以 <xxx.h>  命名 , 而是 ...

  5. C++primer 第 3 章 字符串、向量和数组 3 . 3 标准库类型vector

    标准库类型vector表示对象的集合,其中所有对象的类型都相同.集合中的每个对象都有一个与之对应的索引,索引用于访问对象.因为vector"容纳着"其他对象,所以它也常被称作容器( ...

  6. C++primer 第 3 章 字符串、向量和数组 3.1 命名空间的using声明 3.2标准库类型string

    引言 除了第2章介绍的内置类型之外,C++语言还定义了 -个内容丰富的抽象数据类型库.其中,string和 vector是两种最重耍的标准库类型,前者支持可变长字符串,后者则 表示可变长的集合.还有- ...

  7. Go 标准库 http.FileServer 实现静态文件服务

    文章目录 源码解析 http.Dir() http.FileServer() 支持子目录路径 http.FileServer方法属于标准库 net/http,返回一个使用 FileSystem 接口 ...

  8. c语言标准库assert,C 标准库 - assert.h

    assert.h是c标准库的一个头文件,该头文件的主要目的就是提供一个assert的宏定义. assert只是对所给的表达式求值,就像if判断语句中一样,然后如果该值为真则正常运行,否则报错,并调用a ...

  9. 为什么写了value属性 jq赋值value值不显示_[Go基础]理解 Go 标准库中的 atomic.Value 类型

    转载声明 文章作者:喵叔 上次更新:2019-03-15 许可协议:CC BY-NC-ND 4.0(转载请注明出处) 原文链接:https://blog.betacat.io/post/golang- ...

最新文章

  1. Spring bean 装配
  2. ubuntu openstack spice
  3. POJ - 3422 Kaka's Matrix Travels(网络流-最大费用最大流+拆点法)
  4. 第一阶段冲刺(第七天)
  5. c语言开发移动通信,基于ARM的高效C语言编程
  6. java中的BigInteger
  7. Python基础之变量和常量
  8. Linux将文件复制粘贴到另外一个位置
  9. 女儿傻 女儿悲 2014-2-23
  10. IM即时通讯源码系统安卓苹果IOS双端源码介绍
  11. 深度学习在视频行为识别中应用
  12. mac虚拟机安装win10
  13. 什么是SSR(服务器渲染)
  14. 用户登录页面以及后台方法、拦截器
  15. 学习javaweb第四天
  16. Windows 7下Git SSH 创建Key的步骤(by 星空武哥)
  17. JavaScript基础知识全总结
  18. 快鲸公寓管理系统:职业房东、公寓运营商的共同选择
  19. pytorch保存模型方法
  20. 基于QT4的智能温度采集控制系统

热门文章

  1. 转:阿里CEO张勇:领导者要善于“从后排把人往前拨”
  2. pythone IO.py 文件源代码
  3. 余光中老爷爷走好!!!
  4. !! MACD战法总结
  5. 敲完第一万行代码我发现一个秘密
  6. com.alibaba.fastjson.JSONException: syntax error, expect {, actual [, pos 64, fieldName ***, fasjson
  7. 解决Incorrect result size: expected 1, actual 0!
  8. 2.flex 容器属性 flex-direction ,flex-wrap ,flex-flow
  9. iOS 底层探索篇 —— KVC 底层原理
  10. 四叉树 java 实现