Kotlin之契约Contracts

Kotlin的Contracts是1.3引入的新功能,虽然还是试验阶段,但是在Kotlin的stdlib中已经有多处使用了(例如各种作用域函数)。本文将带领大家解开这个神秘的“契约”。

1. Contracts能做什么?


As smart as the compiler is, it doesn’t always come to the best conclusion.

Contracts可以辅助我们做一些编译器无法完成的效果。例如如下代码

data class Request(val arg: String)
 
class Service {
 
    fun process(request: Request?) {
        validate(request)
        println(request.arg) // Doesn't compile because request might be null
    }
}
 
private fun validate(request: Request?) {
    if (request == null) {
        throw IllegalArgumentException("Undefined request")
    }
    if (request.arg.isBlank()) {
        throw IllegalArgumentException("No argument is provided")
    }
}

从代码实现我们知道,validate()正常执行结束不抛异常的话,request.arg一定不为null,但是在其后的println()中编译器让然将其识别成了nullable类型。

此时我们可以通过Contracts告诉编译器:经validate()处理之后的参数一定不为null

@ExperimentalContracts
class Service {
 
    fun process(request: Request?) {
        validate(request)
        println(request.arg) // Compiles fine now
    }
}
 
@ExperimentalContracts
private fun validate(request: Request?) {
    contract {
        returns() implies (request != null)
    }
    if (request == null) {
        throw IllegalArgumentException("Undefined request")
    }
    if (request.arg.isBlank()) {
        throw IllegalArgumentException("No argument is provided")
    }
}

2. Contracts APIs


Contracts的API符合以下结构:

function {
    contract {
        Effect
    }
}

上述结构的语义是

调用函数function后会产生Effect

目前Effect的Contract目前有两种类型:

  • Returns Contracts
  • CallInPlace Contracts

2.1 Returns Contracts

Specify that if the target function returns, the target condition is satisfied.

以下语法告诉编译器:当返回值是$value时,$condition 条件成立

contract {
	returns($value) implies($condition)
}

例如:

data class MyEvent(val message: String)

@ExperimentalContracts
fun processEvent(event: Any?) {
    if (isInterested(event)) {
        println(event.message) 
    }
}
 
@ExperimentalContracts
fun isInterested(event: Any?): Boolean {
    contract { 
        returns(true) implies (event is MyEvent)
    }
    return event is MyEvent
}

上面代码帮助编译器在processEvent()中完成smart cast。
returns中的value目前只接受true、false、null等类型;
implies中的条件表达式返回布尔结果,表达式必须必须来自以下几种:

  • 空检查(== null, != null)
  • 类型检查(is, !is)
  • 逻辑运算符(&&, ||, !)

还有一种变体形式,针对任何非空返回值:

contract {
    returnsNotNull() implies (event is MyEvent)
}

2.2 CallsInPlace Contract

Ensure that the given block is guaranteed to be called and called only once

例如以下代码,编译器无法判断block中的赋值是否会执行0次或多次,所以会报错

inline fun <R> myRun(block: () -> R): R {
    return block()
}

fun callsInPlace() {
    val i: Int
    myRun {
        i = 1 // Is forbidden due to possible re-assignment
    }
    println(i) // Is forbidden because the variable might be uninitialized
}

通过如下方式,可以告诉编译器block有且仅有一次执行,可以避免以上报错

@ExperimentalContracts
inline fun <R> myRun(block: () -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block()
}

Kotlin的各种作用域函数(run, with, apply等)中都是用了上述Contracts,例如:

@kotlin.internal.InlineOnly
public inline fun <T> T.apply(block: T.() -> Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block()
    return this
}

除了EXACTLY_ONCE之外,还有AT_LEAST_ONCE, AT_MOST_ONCE, UNKNOWN等几种选择

3. Contracts 使用限制


Contracts的使用目前有以下限制

  • 我们只能在top-level的函数中使用Contracts,不能用在类的成员方法上
  • 函数体中如果有Contracts,必须出现在第一句
  • 编译器无条件地信任Contracts,所以开发者必须保证Contracts的正确性
  • Contract中只能引用参数本身,例如下面这个写法会报错
data class Request(val arg: String?)
 
@ExperimentalContracts
private fun validate(request: Request?) {
    contract {
        // We can't reference request.arg here
        returns() implies (request != null && request.arg != null)
    }
    if (request == null) {
        throw IllegalArgumentException("Undefined request")
    }
    if (request.arg.isBlank()) {
        throw IllegalArgumentException("No argument is provided")
    }
}

4. 总结


Contracts的可以补强一些编译器的不足,虽然还处于实验状态,但是Kotlin源码中已经广泛使用,所以API已相对稳定,即使未来API有变化,修改成本也不会很大, 建议大家在适合的地方不妨尝试一下,特别是配合一些DSL的使用中会非常方便。

发布了116 篇原创文章 · 获赞 15 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/vitaviva/article/details/105183951