总结
let、with、run、apply、also 这几个的作用就是在一个对象的上下文中执行一段代码。当我们使用 lambda 表达式在一个对象上调用这样的一个函数时,它就形成了一个暂时的域,在这个域中,可以在不使用变量名的情况下获取到对象,因此,这些函数也被称为作用域函数(Scope functions)
run、with 和 apply 的上下文对象是 this ,可以省略掉 this 单词,因此主要用于操作对象成员(例如调用对象的方法或使用其属性)的时候
let 和 also 的上下文对象是 it , 适用于将此对象作为方法调用参数传入
apply 和 also 返回上下文对象,可用于链式调用和返回上下文对象的函数的返回语句
let、run 和 with 返回 lambda 结果,可以在将结果分配给变量时使用
方法 | 上下文对象 | 返回值 | 扩展函数 | 使用场景 |
---|---|---|---|---|
let | it | lambda 结果 | 是 | 在非空对象上执行一个 lambda或者在局部域中引入一个变量 |
with | this | lambda 结果 | 否 | 在一个对象上组合函数调用 |
run | this | lambda 结果 | 是 | 对象配置并计算结果 |
apply | this | 上下文对象 | 是 | 对象配置 |
also | it | 上下文对象 | 是 | 额外的效果 |
非扩展函数 run | 无 | lambda 结果 | 否 | 在需要表达式的地方运行语句 |
表格里的上下文对象中,this 是可以省略的, it 是隐含的默认参数名,可以显式地指定为其他名字
let
@kotlin.internal.InlineOnly
public inline fun <T, R> T.let(block: (T) -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block(this)
}
可以看到第一句代码是 Contract ,这就需要引出 Contract 的内容
Contract 示例
fun String?.isNull(): Boolean {
return this == null
}
fun test() {
var str: String? = null
if (str.isNull()) {
str = "kotlin contract"
}
println(str.length)
}
如上的代码中,先为 String? 定义一个扩展函数 isNull(),用于判断是否为 null,在 test 函数中,声明一个 String? 类型的 str ,使用 isNull() 判断,如果其为 null ,就为其赋值,这样,在下面调用 str.length 的时候 str 就一定不会为 null ,但实际上,这里还是会编译报错:
Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type String?
这是因为编译器无法分析每个函数,不能得到 str 不为 null 的结果,也就无法将 String? 智能转换为 String ,Contract 就可以解决问题,它可以向编译器通知函数行为,上面的代码修改为如下,就不会报错了
@ExperimentalContracts
fun String?.isNull(): Boolean {
//下面是添加的内容
contract {
returns(false) implies (this@isNull != null)
}
//上面是添加的内容
return this == null
}
@ExperimentalContracts
fun test() {
var str: String? = null
if (str.isNull()) {
str = "kotlin contract"
}
println(str.length)
}
上面代码中添加内容是告诉编译器:如果返回值为 false ,那么 this(函数的接收者)不为 null
Contract 的概念
Contract 是一种向编译器通知函数行为的方法,有以下特点:
- 只能在 top-level 函数体内使用 Contract
- Contract 所调用的声明必须是函数体内第一条语句
- Kotlin 编译器并不会验证 Contract,因此必须编写正确合理的 Contract
- 内联化的函数(也需要是 top-level 层级的函数)支持使用 Contract
Contract 的分类
Returns Contracts
表示当 return 的返回值是某个值(例如true、false、null)时,implies 后面的条件成立,有以下几种形式:
形式 | 说明 |
---|---|
returns(value: Any?) implies 条件 | 如果函数返回值为 value,条件成立 |
returns() implies 条件 | 如果函数能够正常返回,且没有抛出异常,条件成立 |
returnsNotNull implies 条件 | 如果函数返回非 null 值,条件成立 |
CallsInPlace Contracts
CallsInPlace Contracts 允许开发者对调用的 lambda 表达式进行频率上的约束,只能在 inline 函数中调用
前面的高阶函数 let 就是一个 CallsInPlace Contracts
@kotlin.internal.InlineOnly
public inline fun <T, R> T.let(block: (T) -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block(this)
}
contract() 中的 callsInPlace 会告诉编译器,lambda 表达式 block 在 let 函数内只会执行一次
callsInPlace() 中的 InvocationKind 是一个枚举类,包含如下的枚举值
枚举值 | 说明 |
---|---|
AT_MOST_ONCE | 函数参数调用次数 <= 1 |
EXACTLY_ONCE | 函数参数调用次数 == 1 |
AT_LEAST_ONCE | 函数参数调用次数 >= 1 |
UNKNOWN | 函数参数调用次数 不限制 |
以上 contract 内容就结束了,下面看 let 函数的实现,首先要明白两点:
- 在扩展函数内部,你可以像成员函数那样使用 this 来引用接收者对象
- 当 lambda 表达式只有一个参数,可以用 it 关键字来引用唯一的实参
源码
上面的源代码里可以看到:
let 函数是类型 T 的扩展函数,返回类型为 R,只有一个参数,即 block,类型为 (T) -> R,指代 参数为 T ,返回值为 R 的函数,因此上下文对象为 it ,指代 block 的唯一参数 T 。返回值为 block(this) ,是调用 block 指代的函数,并返回 block 函数的返回值,也就是返回 lambda 的结果
示例
在非空对象上执行一个 lambda
val str: String? = "Hello"
//processNonNullString(str) // compilation error: str can be null
val length = str?.let {
println("let() called on $it")
processNonNullString(it) // OK: 'it' is not null inside '?.let { }'
it.length
}
为了提高可读性,在局部域中引入一个变量
val numbers = listOf("one", "two", "three", "four")
val modifiedFirstItem = numbers.first().let { firstItem ->
println("The first item of the list is '$firstItem'")
if (firstItem.length >= 5) firstItem else "!" + firstItem + "!"
}.uppercase()
println("First item after modifications: '$modifiedFirstItem'")
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()
}
with 函数不是扩展函数,而是将类型 T 的一个对象 receiver 作为参数传入 with 函数,同时传入 block 参数,block 的类型为 T.() -> R ,即 类型 T 的一个无参且返回值为类型 R 的扩展函数,而 block 函数的调用者就是前面传入的 receiver 参数,因此上下文对象为 this ,指代扩展函数的接收者 receiver ,返回值为 receiver.block() ,是调用 block 指代的函数,并返回 block 函数的返回值,也就是返回 lambda 的结果
示例
推荐使用情况:调用对象的方法和属性,但不返回结果。意味着 “with this object, do the following”
val numbers = mutableListOf("one", "two", "three")
with(numbers) {
println("'with' is called with argument $this")
println("It contains $size elements")
}
使用对象的属性或方法计算出一个结果
val numbers = mutableListOf("one", "two", "three")
val firstAndLast = with(numbers) {
"The first element is ${first()}," +
" the last element is ${last()}"
}
println(firstAndLast)
run
run 有两个,一个是扩展函数 run ,一个是非扩展函数 run
源码
扩展函数 run
@kotlin.internal.InlineOnly
public inline fun <T, R> T.run(block: T.() -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block()
}
run 函数是类型 T 的一个扩展函数,返回值类型为 R,只有一个参数,即 block,类型为 T.() -> R ,即 T 的一个无参且返回值类型为 R 的扩展函数,因此上下文对象为 this ,指代调用 block 扩展函数的接收者,也就是调用 run 函数的对象,返回值为 block() ,是调用 block 指代的函数,并返回 block 函数的返回值,也就是返回 lambda 的结果
非扩展函数 run
@kotlin.internal.InlineOnly
public inline fun <R> run(block: () -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block()
}
run 函数不是扩展函数,返回类型为 R , run 函数的唯一参数 block 的类型为 () -> R ,即 一个无参且返回类型为 R 的函数类型,因此 没有上下文对象,返回值为 block() ,是调用 block 指代的函数,并返回 block 函数的返回值,也就是返回 lambda 的结果
示例
扩展函数 run
同时包含对象初始化和返回值的计算
val service = MultiportService("https://example.kotlinlang.org", 80)
val result = service.run {
port = 8080
query(prepareRequest() + " to port $port")
}
非扩展函数 run
需要返回值的情况下执行由多个语句组成的块
val hexNumberRegex = run {
val digits = "0-9"
val hexDigits = "A-Fa-f"
val sign = "+-"
Regex("[$sign]?[$digits$hexDigits]+")
}
for (match in hexNumberRegex.findAll("+123 -FFFF !%*& 88 XYZ")) {
println(match.value)
}
apply
源码
@kotlin.internal.InlineOnly
public inline fun <T> T.apply(block: T.() -> Unit): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block()
return this
}
apply 函数是类型 T 的一个扩展函数,且其返回值类型为 T 类型,唯一的参数 block 的类型为 T.() -> Unit,即 T 的一个无参无返回值扩展函数,因此上下文对象为 this ,指代调用 block 扩展函数的接收者,也就是调用 apply 函数的对象 ,返回值为 this,也就是调用者,即返回上下文对象
示例
对象配置。意味着 “apply the following assignments to the object”
val adam = Person("Adam").apply {
age = 32
city = "London"
}
println(adam)
also
源码
public inline fun <T> T.also(block: (T) -> Unit): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block(this)
return this
}
also 函数是类型 T 的一个扩展函数,且其返回值类型为 T 类型,block 参数的类型为 (T) -> Unit,即 参数为 T 且无返回值的函数,因此上下文对象为 it ,指代 block 的唯一参数 T ,返回值为 this,也就是调用者,即返回上下文对象
示例
将上下文作为参数。意味着 "and also do the following with the object"
val numbers = mutableListOf("one", "two", "three")
numbers
.also { println("The list elements before adding new one: $it") }
.add("four")