kotlin标准库随处可见的contract到底是什么?(代码片段)

bug樱樱 bug樱樱     2022-12-01     598

关键词:

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: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:
        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标准库函数①(apply标准库函数|let标准库函数)(代码片段)

文章目录一、apply标准库函数二、let标准库函数Kotlin语言中,在Standard.kt源码中,为所有类型定义了一批标准库函数,所有的Kotlin类型都可以调用这些函数;一、apply标准库函数Kotlin标准库函数中的apply函数,该函数可以看作实例对象的... 查看详情

kotlin扩展函数③(定义扩展文件|重命名扩展函数|kotlin标准库扩展函数)(代码片段)

文章目录一、定义扩展文件二、重命名扩展函数三、Kotlin标准库扩展函数一、定义扩展文件如果定义的扩展函数需要在多个Kotlin代码文件中使用,则需要在单独的Kotlin文件中定义,该文件被称为扩展文件;定义标准库函数的Standard.kt... 查看详情

kotlin标准库函数④(takeif标准库函数|takeunless标准库函数)(代码片段)

文章目录一、takeIf标准库函数二、takeUnless标准库函数Kotlin语言中,在Standard.kt源码中,为所有类型定义了一批标准库函数,所有的Kotlin类型都可以调用这些函数;一、takeIf标准库函数takeIf函数的返回值由其Lambda表达式参数的返回值确... 查看详情

从 Swift 访问 Kotlin 标准库

】从Swift访问Kotlin标准库【英文标题】:AccessKotlinStandardLibraryfromSwift【发布时间】:2019-04-0520:03:03【问题描述】:在我的KotlinMultiplatform项目中,我试图从Swift访问在kotlin-stdlib中定义的Kotlin类型。TL;DR:StdLib类型/方法似乎不会导致... 查看详情

如何在 Kotlin 标准库(多平台)上获取当前的 unixtime

】如何在Kotlin标准库(多平台)上获取当前的unixtime【英文标题】:HowtogetcurrentunixtimeonKotlinstandardlibrary(multiplatform)【发布时间】:2021-01-2219:54:48【问题描述】:我有一个Kotlin多平台项目,我想在共享代码中获取当前的unixtime。您... 查看详情

kotlin标准库函数总结(apply函数|let函数|run函数|with函数|also函数|takeif函数|takeunless函数)(代码片段)

...so标准库函数六、takeIf标准库函数七、takeUnless标准库函数Kotlin语言中,在Standard.kt源码中,为所有类型定义了一批标准库函数,所有的Kotlin类型都可以调用这些函数;一、apply标准库函数Kotlin标准库函数中的apply函数,该函数可以看作实... 查看详情

在“使用”标准库函数中测试 Kotlin lambda

】在“使用”标准库函数中测试Kotlinlambda【英文标题】:TestKotlinlambdawithin"use"standardlibraryfunction【发布时间】:2019-10-0419:21:14【问题描述】:我正在尝试在传递给使用Kotlin标准库内联函数的lambda函数中对代码进行单元测试... 查看详情

Kotlin 的 Open 关键字是可见性修饰符吗? [复制]

】Kotlin的Open关键字是可见性修饰符吗?[复制]【英文标题】:KotlinistheOpenkeywordavisibilitymodifier?[duplicate]【发布时间】:2021-11-1823:11:09【问题描述】:我是Kotlin的新手,我正在阅读kotlin官方文档,它说明了4个不同的可见性修饰符(... 查看详情

kotlin标准库函数②(run标准库函数|run函数传入lambda表达式作为参数|run函数传入函数引用作为参数)(代码片段)

...入Lambda表达式作为参数2、run函数传入函数引用作为参数Kotlin语言中,在Standard.kt源码中,为所有类型定义了一批标准库函数,所有的Kotlin类型都可以调用这些函数;一、run标准库函数1、run函数传入Lambda表达式作为参数run标准库函数原... 查看详情

kotlin标准库函数总结(apply函数|let函数|run函数|with函数|also函数|takeif函数|takeunless函数)(代码片段)

...so标准库函数六、takeIf标准库函数七、takeUnless标准库函数Kotlin语言中,在Standard.kt源码中,为所有类型定义了一批标准库函数,所有的Kotlin类型都可以调用这些函数;一、apply标准库 查看详情

kotlin—lazy及其原理

参考技术Alazy是属性委托的一种,是有kotlin标准库实现。它是属性懒加载的一种实现方式,在对属性使用时才对属性进行初始化,并且支持对属性初始化的操作时进行加锁,使属性的初始化在多线程环境下线程安全。lazy默认是线... 查看详情

kotlin协程协程底层实现①(kotlin协程分层架构|基础设施层|业务框架层|使用kotlin协程基础设施层标准库api实现协程)(代码片段)

文章目录一、Kotlin协程分层架构二、使用Kotlin协程基础设施层标准库Api实现协程一、Kotlin协程分层架构Kotlin协程分层架构:在Kotlin中,协程分为两层;基础设施层:Kotlin提供了协程标准库Api,为协程提供概念,语义支持,是协程实现的基... 查看详情

kotlin协程协程底层实现①(kotlin协程分层架构|基础设施层|业务框架层|使用kotlin协程基础设施层标准库api实现协程)(代码片段)

文章目录一、Kotlin协程分层架构二、使用Kotlin协程基础设施层标准库Api实现协程一、Kotlin协程分层架构Kotlin协程分层架构:在Kotlin中,协程分为两层;基础设施层:Kotlin提供了协程标准库Api,为协程提供概念,语义支持,是协程实现的基... 查看详情

Kotlin 中布局的可见性

】Kotlin中布局的可见性【英文标题】:visibilityofalayoutinKotlin【发布时间】:2021-02-1108:11:28【问题描述】:我有一个布局,我想在单击按钮时使其可见(首先是“GONE”)。<includeandroid:id="@+id/registration_layout"layout="@layout/user_registra... 查看详情

kotlin1.6.0新特性预览:语法和标准库(代码片段)

Kotlin1.5.30isthelastincrementalreleasebeforeKotlin1.6.0,itincludesmanyexperimentallanguageandstandardlibraryfeaturesthatweareplanningtoreleaseinKotlin1.6.0:sealed when statementsopt-inrequirementsins 查看详情

到底什么是内存可见性?(代码片段)

...弄明白,今天咱们一起看一下,这个可见性,到底是如何可见,数据到底是如何可见的。首先我们要达成一个共识:单核CPU由于同一时刻只会有一个线程执行,而每个线程执行的时候操作的都是 查看详情

Kotlin:使内部函数对单元测试可见

】Kotlin:使内部函数对单元测试可见【英文标题】:Kotlin:Makeaninternalfunctionvisibleforunittests【发布时间】:2016-06-1113:46:02【问题描述】:如果测试与生产代码位于不同的模块中(这很常见),使内部函数对测试可见的最佳方法是什... 查看详情

public 在 kotlin 中显示多余的可见性修饰符

】public在kotlin中显示多余的可见性修饰符【英文标题】:publicshowsredundantvisibilitymodifierinkotlin【发布时间】:2020-12-1522:48:36【问题描述】:我收到一条关于公众的警​​告消息,表明它是一个多余的可见性修饰符。实际上我在不... 查看详情