9.1.4 作用域函数
原文链接: https://kotlinlang.org/docs/scope-functions.html
9.1.4 作用域函数
Kotlin 标准库中包含若干函数,它们的唯一用途就是在某个对象的上下文中执行一段代码。当你在一个对象上调用这类函数并提供一个 lambda 表达式时,它会形成一个临时作用域。在这个作用域中,你无需使用名字就能访问该对象。这类函数称为作用域函数。共有五个:let、run、with、apply 和 also。
基本上,这些函数都执行相同的动作:在某个对象上执行一段代码。区别在于该对象在代码块内部以何种方式可用,以及整个表达式的结果是什么。
下面是一个使用作用域函数的典型示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
| data class Person(var name: String, var age: Int, var city: String) {
fun moveTo(newCity: String) { city = newCity }
fun incrementAge() { age++ }
}
fun main() {
Person("Alice", 20, "Amsterdam").let {
println(it)
it.moveTo("London")
it.incrementAge()
println(it)
}
}
|
如果你不借助 let 编写同样的代码,就必须引入一个新变量,并在每次使用时重复它的名字。
1
2
3
4
5
6
7
8
9
10
11
12
| data class Person(var name: String, var age: Int, var city: String) {
fun moveTo(newCity: String) { city = newCity }
fun incrementAge() { age++ }
}
fun main() {
val alice = Person("Alice", 20, "Amsterdam")
println(alice)
alice.moveTo("London")
alice.incrementAge()
println(alice)
}
|
作用域函数不会带来任何新的技术能力,但它们可以让代码更简洁、更易读。
由于作用域函数之间有许多相似之处,为你的用例挑选合适的那一个可能并不容易。选择主要取决于你的意图以及项目中使用的一致性。下面我们详细说明作用域函数之间的差异及其惯例。
函数选择
为了帮助你选择适合自己目的的作用域函数,我们用下表总结它们之间的关键差异。
| 函数 | 对象引用 | 返回值 | 是否为扩展函数 |
| let | it | lambda 结果 | 是 |
| run | this | lambda 结果 | 是 |
| run | - | lambda 结果 | 否:不接收上下文对象调用 |
| with | this | lambda 结果 | 否:把上下文对象作为参数。 |
| apply | this | 上下文对象 | 是 |
| also | it | 上下文对象 | 是 |
有关这些函数的详细信息,请参阅下面各专门小节。
下面是一份根据用途选择作用域函数的简要指南:
- 在非空对象上执行 lambda:
let - 在局部作用域中把某个表达式引入为变量:
let - 对象配置:
apply - 对象配置并计算结果:
run - 在需要表达式的地方运行多条语句:非扩展形式的
run - 附加效果:
also - 对某个对象上的函数调用进行分组:
with
不同作用域函数的用例存在重叠,因此你可以根据项目或团队中使用的具体惯例来选择使用哪些函数。
虽然作用域函数可以让代码更简洁,但请避免过度使用:它可能让代码难以阅读并导致错误。我们还建议你避免嵌套作用域函数,并在链式调用它们时保持谨慎,因为很容易对当前的上下文对象以及 this 或 it 的值感到困惑。
区别
由于作用域函数在本质上相似,理解它们之间的差异很重要。每个作用域函数之间有两个主要差异:
上下文对象:this 还是 it
在传给作用域函数的 lambda 内部,上下文对象通过简短的引用而不是它的实际名称来使用。每个作用域函数都使用以下两种方式之一来引用上下文对象:作为 lambda 接收者(this),或作为 lambda 参数(it)。两者提供的能力相同,因此我们针对不同的用例说明各自的优缺点,并给出使用建议。
1
2
3
4
5
6
7
8
9
10
11
12
13
| fun main() {
val str = "Hello"
// this
str.run {
println("The string's length: $length")
//println("The string's length: ${this.length}") // does the same
}
// it
str.let {
println("The string's length is ${it.length}")
}
}
|
this
run、with 和 apply 把上下文对象引用为 lambda 接收者——通过关键字 this。因此,在它们的 lambda 中,该对象的可用方式与在普通类函数中一样。
在大多数情况下,访问接收者对象的成员时可以省略 this,让代码更简短。另一方面,如果省略 this,就难以区分接收者成员与外部对象或函数。因此,对于主要通过调用对象函数或给属性赋值来操作对象成员的 lambda,建议把上下文对象作为接收者(this)。
1
2
3
4
5
6
7
8
9
| data class Person(var name: String, var age: Int = 0, var city: String = "")
fun main() {
val adam = Person("Adam").apply {
age = 20 // same as this.age = 20
city = "London"
}
println(adam)
}
|
it
而 let 和 also 把上下文对象引用为 lambda 参数。如果没有指定参数名,则通过隐式的默认名称 it 访问该对象。it 比 this 更短,使用 it 的表达式通常也更易读。
不过,在调用对象的函数或属性时,你无法像 this 那样隐式地使用该对象。因此,当对象主要作为函数调用中的参数使用时,通过 it 访问上下文对象更好。如果代码块中使用多个变量,it 也更好。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| import kotlin.random.Random
fun writeToLog(message: String) {
println("INFO: $message")
}
fun main() {
fun getRandomInt(): Int {
return Random.nextInt(100).also {
writeToLog("getRandomInt() generated value $it")
}
}
val i = getRandomInt()
println(i)
}
|
下面的示例演示了如何通过名为 value 的参数把上下文对象引用为 lambda 参数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| import kotlin.random.Random
fun writeToLog(message: String) {
println("INFO: $message")
}
fun main() {
fun getRandomInt(): Int {
return Random.nextInt(100).also { value ->
writeToLog("getRandomInt() generated value $value")
}
}
val i = getRandomInt()
println(i)
}
|
返回值
作用域函数在返回值上有所不同:
apply 和 also 返回上下文对象。let、run 和 with 返回 lambda 结果。
你应该根据代码中接下来想做什么,仔细考虑你想要的返回值。这有助于你选择最合适的作用域函数。
上下文对象
apply 和 also 的返回值就是上下文对象本身。因此,它们可以作为中间步骤加入调用链:你可以继续在同一个对象上一个接一个地链式调用函数。
1
2
3
4
5
6
7
8
9
10
11
12
| fun main() {
val numberList = mutableListOf<Double>()
numberList.also { println("Populating the list") }
.apply {
add(2.71)
add(3.14)
add(1.0)
}
.also { println("Sorting the list") }
.sort()
println(numberList)
}
|
它们也可以用在返回上下文对象的函数的 return 语句中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| import kotlin.random.Random
fun writeToLog(message: String) {
println("INFO: $message")
}
fun main() {
fun getRandomInt(): Int {
return Random.nextInt(100).also {
writeToLog("getRandomInt() generated value $it")
}
}
val i = getRandomInt()
}
|
lambda 结果
let、run 和 with 返回 lambda 结果。因此,当你要把结果赋值给变量、对结果进行链式操作等时,可以使用它们。
1
2
3
4
5
6
7
8
9
| fun main() {
val numbers = mutableListOf("one", "two", "three")
val countEndsWithE = numbers.run {
add("four")
add("five")
count { it.endsWith("e") }
}
println("There are $countEndsWithE elements that end with e.")
}
|
此外,你也可以忽略返回值,用作用域函数为局部变量创建一个临时作用域。
1
2
3
4
5
6
7
8
| fun main() {
val numbers = mutableListOf("one", "two", "three")
with(numbers) {
val firstItem = first()
val lastItem = last()
println("First item: $firstItem, last item: $lastItem")
}
}
|
函数
为了帮助你为用例选择合适的作用域函数,我们对它们进行详细说明并给出使用建议。从技术上讲,作用域函数在许多情况下可以互换,因此示例展示的是使用它们的惯例。
let
- 上下文对象作为参数(
it)可用。 - 返回值是 lambda 结果。
let 可用于在调用链的结果上调用一个或多个函数。例如,下面的代码打印对某个集合执行两次操作的结果:
1
2
3
4
5
| fun main() {
val numbers = mutableListOf("one", "two", "three", "four", "five")
val resultList = numbers.map { it.length }.filter { it > 3 }
println(resultList)
}
|
借助 let,你可以把上面的例子改写为不把列表操作的结果赋值给变量:
1
2
3
4
5
6
7
| fun main() {
val numbers = mutableListOf("one", "two", "three", "four", "five")
numbers.map { it.length }.filter { it > 3 }.let {
println(it)
// 如果需要,还可以继续添加更多函数调用
}
}
|
如果传给 let 的代码块只包含一个以 it 为参数的函数,你可以用方法引用(::)代替 lambda 参数:
1
2
3
4
| fun main() {
val numbers = mutableListOf("one", "two", "three", "four", "five")
numbers.map { it.length }.filter { it > 3 }.let(::println)
}
|
let 常用于执行包含非空值的代码块。要对可空对象执行操作,请在其上使用安全调用运算符 ?.,并在其 lambda 中调用带有这些操作的 let。
1
2
3
4
5
6
7
8
9
10
11
| fun processNonNullString(str: String) {}
fun main() {
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
}
}
|
你还可以用 let 引入作用域有限的局部变量,让代码更易读。要为上下文对象定义一个局部变量,请把它的名字作为 lambda 参数提供,这样就可以用它代替默认的 it。
1
2
3
4
5
6
7
8
| fun main() {
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
- 上下文对象作为接收者(
this)可用。 - 返回值是 lambda 结果。
由于 with 不是扩展函数:上下文对象作为参数传入,但在 lambda 内部,它作为接收者(this)可用。
当你不需要使用返回结果时,我们建议使用 with 来调用上下文对象上的函数。在代码中,with 可以读作“对于这个对象,执行以下操作。”
1
2
3
4
5
6
7
| fun main() {
val numbers = mutableListOf("one", "two", "three")
with(numbers) {
println("'with' is called with argument $this")
println("It contains $size elements")
}
}
|
你还可以用 with 引入一个辅助对象,使用它的属性或函数来计算某个值。
1
2
3
4
5
6
7
8
| fun main() {
val numbers = mutableListOf("one", "two", "three")
val firstAndLast = with(numbers) {
"The first element is ${first()}," +
" the last element is ${last()}"
}
println(firstAndLast)
}
|
run
- 上下文对象作为接收者(
this)可用。 - 返回值是 lambda 结果。
run 与 with 做同样的事,但它是作为扩展函数实现的。因此,就像 let 一样,你可以用点号表示法在上下文对象上调用它。
当你的 lambda 既要初始化对象,又要计算返回值时,run 很有用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| class MultiportService(var url: String, var port: Int) {
fun prepareRequest(): String = "Default request"
fun query(request: String): String = "Result for query '$request'"
}
fun main() {
val service = MultiportService("https://example.kotlinlang.org", 80)
val result = service.run {
port = 8080
query(prepareRequest() + " to port $port")
}
// the same code written with let() function:
val letResult = service.let {
it.port = 8080
it.query(it.prepareRequest() + " to port ${it.port}")
}
println(result)
println(letResult)
}
|
你也可以把 run 作为非扩展函数调用。非扩展形式的 run 没有上下文对象,但它仍然返回 lambda 结果。非扩展 run 让你可以在需要表达式的地方执行由多条语句组成的代码块。在代码中,非扩展 run 可以读作“运行该代码块并计算结果。”
1
2
3
4
5
6
7
8
9
10
11
12
13
| fun main() {
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
- 上下文对象作为接收者(
this)可用。 - 返回值是对象本身。
由于 apply 返回上下文对象本身,我们建议把它用于不返回值、且主要操作接收者对象成员的代码块。apply 最常见的用例是对象配置。这类调用可以读作“对该对象应用以下赋值。”
1
2
3
4
5
6
7
8
9
| data class Person(var name: String, var age: Int = 0, var city: String = "")
fun main() {
val adam = Person("Adam").apply {
age = 32
city = "London"
}
println(adam)
}
|
apply 的另一个用例是把它包含在多个调用链中,以实现更复杂的处理。
also
- 上下文对象作为参数(
it)可用。 - 返回值是对象本身。
also 适合执行一些把上下文对象作为参数的操作。对于需要引用对象本身而不是它的属性和函数的操作,或者当你不想遮蔽外层作用域中的 this 引用时,请使用 also。
当你在代码中看到 also 时,可以把它读作“并且还对该对象执行以下操作。”
1
2
3
4
5
6
| fun main() {
val numbers = mutableListOf("one", "two", "three")
numbers
.also { println("The list elements before adding new one: $it") }
.add("four")
}
|
takeIf 与 takeUnless
除了作用域函数之外,标准库还包含 takeIf 和 takeUnless 函数。这些函数让你可以在调用链中嵌入对对象状态的检查。
当与谓词一起在某个对象上调用时,如果该对象满足给定的谓词,takeIf 就返回该对象,否则返回 null。因此,takeIf 是针对单个对象的过滤函数。
takeUnless 的逻辑与 takeIf 相反。当与谓词一起在某个对象上调用时,如果该对象满足给定的谓词,takeUnless 返回 null,否则返回该对象。
使用 takeIf 或 takeUnless 时,对象作为 lambda 参数(it)可用。
1
2
3
4
5
6
7
8
9
| import kotlin.random.*
fun main() {
val number = Random.nextInt(100)
val evenOrNull = number.takeIf { it % 2 == 0 }
val oddOrNull = number.takeUnless { it % 2 == 0 }
println("even: $evenOrNull, odd: $oddOrNull")
}
|
提示: 在 takeIf 和 takeUnless 之后链式调用其他函数时,别忘了执行 null 检查或使用安全调用(?.),因为它们的返回值是可空的。
1
2
3
4
5
6
| fun main() {
val str = "Hello"
val caps = str.takeIf { it.isNotEmpty() }?.uppercase()
//val caps = str.takeIf { it.isNotEmpty() }.uppercase() //编译错误
println(caps)
}
|
takeIf 和 takeUnless 与作用域函数组合使用时特别有用。例如,你可以把 takeIf 和 takeUnless 与 let 链接起来,在满足给定谓词的对象上运行代码块。为此,在对象上调用 takeIf,然后用安全调用(?)调用 let。对于不满足谓词的对象,takeIf 返回 null,因此不会调用 let。
1
2
3
4
5
6
7
8
9
10
11
| fun main() {
fun displaySubstringPosition(input: String, sub: String) {
input.indexOf(sub).takeIf { it >= 0 }?.let {
println("The substring $sub is found in $input.")
println("Its start position is $it.")
}
}
displaySubstringPosition("010000011", "11")
displaySubstringPosition("010000011", "12")
}
|
作为对比,下面是同一个函数在不使用 takeIf 或作用域函数时的写法:
1
2
3
4
5
6
7
8
9
10
11
12
| fun main() {
fun displaySubstringPosition(input: String, sub: String) {
val index = input.indexOf(sub)
if (index >= 0) {
println("The substring $sub is found in $input.")
println("Its start position is $index.")
}
}
displaySubstringPosition("010000011", "11")
displaySubstringPosition("010000011", "12")
}
|