kotlin 作用域函数
在Kotlin标准库(Standard.kt)中定义了几个作用域函数,其中包含let、run、with、apply和also。这几个函数有一个共同点就是在一个对象的上下文中执行代码块。
当对一个对象调用一个函数并提供一个lambda表达式时,它会形成一个临时作用域。在此作用域中,可以访问该对象而无需其名称。这样的函数称之为
作用域函数。
这些函数使用起来比较相似,主要区别在于两个方面:
- 应用上下文对象的方式
- 返回值
let
public inline fun <T, R> T.let(block: (T) -> R): R
let声明为扩展函数,上下文对象作为lambda表达式的参数(默认为it,也可以自定义名称),返回值是lambda表达式的结果。
val result = "a".let {
123
// return@let 123
}
print(result) // 123
上面的代码会输出123,在lambda表达式中可以省略return语句,默认最后一行代码为返回值。
let函数经常用于对象非空执行代码块的情况。例如下面情况使用let是非常方便的。
val str: String? = "Hello"
//processNonNullString(str) // 编译错误:str 可能为空
val length = str?.let {
println("let() called on $it")
processNonNullString(it) // 编译通过:'it' 在 '?.let { }' 中必不为空
it.length
}
当运行时str不为空才会执行let后面的代码块,相比Java中需要对str进行非空判断就非常便捷了。
run
public inline fun <R> run(block: () -> R): R
public inline fun <T, R> T.run(block: T.() -> R): R
再标准库中定义了两个run函数,其中第一个run函数可以独立运行一个代码块,并将lambda表达式的返回值作为run函数的返回值。例如:
val hexNumberRegex = run {
val digits = "0-9"
val hexDigits = "A-Fa-f"
val sign = "+-"
Regex("[$sign]?[$digits$hexDigits]+")
}
for (match in hexNumberRegex.findAll("+1234 -FFFF not-a-number")) {
println(match.value)
}
第二个run函数是一个扩展函数,上下文对象作为接收者(this) 来访问,返回值是lambda表达式结果。
val service = MultiportService("https://example.kotlinlang.org", 80)
val result = service.run {
port = 8080
query(prepareRequest() + " to port $port")
}
// 同样的代码如果用 let() 函数来写:
val letResult = service.let {
it.port = 8080
it.query(it.prepareRequest() + " to port ${it.port}")
}
可以看出这个run函数与let类似,区别在于run中可以直接使用上下文对象的属性和方法,而let需要通过it来调用上下文对象的属性和方法。
with
public inline fun <T, R> with(receiver: T, block: T.() -> R): R
with函数是一个非扩展函数,将上下文对象作为参数传递,并接收一个lambda表达式,在lambda表达式内部可以直接引用上下文对象的属性和方法,并将lambda表达式结果作为with函数的返回值。
val numbers = mutableListOf("one", "two", "three")
with(numbers) {
println("'with' is called with argument $this")
println("It contains $size elements")
}
with函数可以理解为“对于这个对象执行以下操作”。在使用with函数时建议使用 with 来调用上下文对象上的函数,而不使用 lambda 表达式结果。
apply
public inline fun <T> T.apply(block: T.() -> Unit): T
apply函数是一个扩展函数,上下文对象 作为接收者(this)来访问。 返回值 是上下文对象本身。
apply 的常见情况是对象配置。这样的调用可以理解为“将以下赋值操作应用于对象”。
val adam = Person("Adam").apply {
age = 32
city = "London"
}
println(adam)
also
public inline fun <T> T.also(block: (T) -> Unit): T
also函数是一个扩展函数,上下文对象作为 lambda 表达式的参数(it)来访问。 返回值是上下文对象本身。
also 对于执行一些将上下文对象作为参数的操作很有用。 对于需要引用对象而不是其属性与函数的操作,或者不想屏蔽来自外部作用域的 this 引用时,请使用 also。
当你在代码中看到 also 时,可以将其理解为“并且用该对象执行以下操作”。
val numbers = mutableListOf("one", "two", "three")
numbers
.also { println("The list elements before adding new one: $it") }
.add("four")
总结
对于各个函数之间的区别可以参考下面的表格。根据使用场景选择合适的函数。
| 函数 | 对象引用 | 返回值 | 是否时扩展函数 |
|---|---|---|---|
| let | it | Lambda 表达式结果 | 是 |
| run | this | Lambda 表达式结果 | 是 |
| run | - | Lambda 表达式结果 | 不是:调用无需上下文对象 |
| with | this | Lambda 表达式结果 | 不是:把上下文对象当做参数 |
| apply | this | 上下文对象 | 是 |
| also | it | 上下文对象 | 是 |
以下是根据预期目的选择作用域函数的简短指南:
- 对一个非空(non-null)对象执行 lambda 表达式:
let - 将表达式作为变量引入为局部作用域中:
let - 对象配置:
apply - 对象配置并且计算结果:
run - 在需要表达式的地方运行语句:非扩展的
run - 附加效果:
also - 一个对象的一组函数调用:
with
参考