携手创作,一起生长!这是我参与「日新计划 8 月更文挑战」的第5天,点击检查活动详情。

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 就指定的此类型,由于其「重复」次数由参数传入,确实不知道;而除它外,其他像 letwith 这些,都是用的InvocationKind.EXACTLY_ONCE,即单次履行。

Effect

Effect 接口类型,表明一个办法的履行协议约定,其不同子接口,对应不同的协议类型,前面提到的 ReturnsReturnsNotNullCallsInPlace 均为它的子类型。

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。isAvailableString? 类型的扩展,用以判别是否一个字符串非空且长度大于 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 官方 去掉的。