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")
}