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的使用中会非常方便。