Kotlin 标准库随处可见的 contract 到底是什么?
Posted bug樱樱
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Kotlin 标准库随处可见的 contract 到底是什么?相关的知识,希望对你有一定的参考价值。
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 Effect
public interface ConditionalEffect : Effect
public interface SimpleEffect : Effect
public infix fun implies(booleanExpression: Boolean): ConditionalEffect
public interface Returns : SimpleEffect
public interface ReturnsNotNull : SimpleEffect
public interface CallsInPlace : Effect
简单明了,全员接口!来看一个官方使用,以便理解下这些接口的意义和使用:
public inline fun Array<*>?.isNullOrEmpty(): Boolean
contract
returns(false) implies (this@isNullOrEmpty != null)
return this == null || this.isEmpty()
这里涉及到两个 Effect:Returns
和 ConditionalEffect
。此方法的功能为:判断数组为 null 或者是无元素空数组。它的 contract 约定是这样的:
- 调用
returns(value: Any?)
获得Returns
协议(当然也就是 SimpleEffect 协议),其传入值是 false - 第1步的 Returns 调用
implies
方法,条件是「本对象非空」,得到了一个ConditionalEffect
- 于是,最终协议的意思是:函数返回 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:
String
Found:
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 到底是什么?的主要内容,如果未能解决你的问题,请参考以下文章
Kotlin标准库函数 ④ ( takeIf 标准库函数 | takeUnless 标准库函数 )
Kotlin标准库函数 ③ ( with 标准库函数 | also 标准库函数 )
如何在 Kotlin 标准库(多平台)上获取当前的 unixtime