1. 起因
在使用一个工具类时,发现编辑器的自动类型推断不好使了,先来个简单的例子
val str : String ? = ""
if(str.isNotNull()){
println(str.length)
}
private fun String?.isNotNull(): Boolean{
return this != null
}
你猜编译器能不能正常推断,很遗憾,并不能 但是不用函数把判断条件用函数封装起来,编译器又可以正常推断 但是kotlin中with(),let(),apply()等高阶函数中也是函数,我们在使用的过程中可以正常进行类型推断,今天翻了下代码,下面是跟踪过程。
2.代码跟踪(以with()为例)
@kotlin.internal.InlineOnly
public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
//作用未知,
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
//进行函数调用
return receiver.block()
}
上面代码中出现了contract代码块,奇怪,直接调用receiver.block()不就行了吗,为什么要多此一举,继续跟踪contract,发现contract是一个内联函数,接收一个ContractBuilder的拓展函数作为参数
/**
指定函数的协定。
合约描述必须位于函数的开头,并且至少具有一种效果。
目前只有顶级函数可以有协定。
参数:
生成器 - 在 ContractBuilder 成员的帮助下描述函数合约的 lambda
**/
@ContractsDsl
@ExperimentalContracts //实验性的特性
@InlineOnly
@SinceKotlin("1.3") // kotlin 1.3 开始
@Suppress("UNUSED_PARAMETER")
public inline fun contract(builder: ContractBuilder.() -> Unit) { }
这里我们看到contract和ContractBuilder这类相关,也就是ContractBuilder
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
}
可以看到,当前 Contract 有两种类型:
- Returns Contracts
- CallInPlace Contracts
2.1 returns contract
Returns Contracts 有以下几种形式:
- returns(true) implies
- returns(false) implies
- returns(null) implies
- returns implies
- returnsNotNull implies
跟踪returns类
@ContractsDsl
@ExperimentalContracts
@SinceKotlin("1.3")
public interface Returns : SimpleEffect
可以看到returns 是一个继承与Effect接口的接口 我们继续看Effect的接口和它子类,继承图如下
其他几个类型按照字面意思很好理解,returns implies 怎么理解呢?
public interface SimpleEffect : Effect {
/**
*指定这种效果在观察时保证[booleanexpression]为真。
*注意:[boolean expression]只能接受布尔表达的子集,
*函数参数或接收器(`this')经历
* - 虚假检查的真实情况,以防参数或接收器为`boolean';
* - null -checks(`== null`,`!= null`);
* - 实例检查(`是`,`,`!is');
* - 借助逻辑运算符(`&`,'||`,`!`)的组合。
*/
@ContractsDsl
@ExperimentalContracts
public infix fun implies(booleanExpression: Boolean): ConditionalEffect
}
通过翻译注解可知, returns implies 表示当该函数正常返回时,implies后面的条件成立。
2.2 CallInPlace Contracts
@ContractsDsl
public fun <R> callsInPlace(lambda: Function<R>,
kind: InvocationKind = InvocationKind.UNKNOWN): CallsInPlace
callsInPlace() 中的 InvocationKind 是一个枚举类,包含如下的枚举值:
- AT_MOST_ONCE:函数参数将被调用一次或根本不调用。
- EXACTLY_ONCE:函数参数将只被调用一次。
- AT_LEAST_ONCE:函数参数将被调用一次或多次。
- UNKNOWN:一个函数参数它可以被调用的次数未知。
Kotlin 的 Scope Function 都使用了上述 Contracts。 callsInPlace() 允许开发者提供对调用的 lambda 表达式进行时间/位置/频率上的约束。 上面
Effect 表示函数调用的效果。每当调用一个函数时,它的所有效果都会被激发。编译器将收集所有激发的效果以便于其分析。 目前 Kotlin 只支持有 4 种 Effect:
- Returns: 表示函数成功返回,不会不引发异常。
- ReturnsNotNull:表示函数成功返回不为 null 的值。
- ConditionalEffect:表示一个效果和一个布尔表达式的组合,如果触发了效果,则保证为true。
- CallsInPlace:表示对传递的 lambda 参数的调用位置和调用次数的约束。
3. contract的使用
了解了基本原理,我们再来使用contract解决刚刚自定义的String拓展方法isNotNull()不能正常类型推断的问题
////加上Experimental注解,否则编译不通过
@OptIn(ExperimentalContracts::class)
private fun String?.isNotNull(): Boolean{
contract{
returns(true) implies (this@isNotNull!= null)
}
return this != null
}
发现可以正常使用了
4. 总结和注意事项
contract 为开发者解决了编译器不够智能的问题,这样可以使代码更简练,更加通俗易懂。但是这个智能的做法是通过开发者主观代码告诉编译器的,编译器无条件地遵守这个约定,这也就为开发者提出了额外的要求,那就是一定要确保 contract 的正确性,不然将会导致很多不可控制的错误,甚至是崩溃
4.1 概念
Contract 是一种向编译器通知函数行为的方法。 Contract 是 Kotlin1.3 的新特性,在当前 Kotlin 1.4 时仍处于试验阶段。 可用于智能类型推断,简化代码
4.2 特性
- 只能在 top-level 函数体内使用 Contract,不能在成员和类函数上使用它们。
- Contract 所调用的声明必须是函数体内第一条语句。
- 目前 Kotlin 编译器并不会验证 Contract,因此开发者有责任编写正确合理的 Contract。
在 Kotlin 1.4 中,对于 Contract 有两项改进:
- 支持使用内联特化的函数来实现契约
- Kotlin 1.3 不能为成员函数添加 Contract,从 Kotlin 1.4 开始支持为 final 类型的成员函数添加 Contract(当然任意成员函数可能存在被覆写的问题,因而不能添加)。
参考链接