作用域函数

Kotlin 标准库包含几个函数,它们的唯一目的是在对象的上下文中执行代码块。当对一个对象调用这样的函数并提供一个 lambda 表达式时,它会形成一个临时作用域。在此作用域中,可以访问该对象而无需其名称。这些函数称为作用域函数。共有以下五种:letrunwithapply 以及 also

这些函数基本上做了同样的事情:在一个对象上执行一个代码块。不同的是这个对象在块中如何使用,以及整个表达式的结果是什么。

下面是作用域函数的典型用法:

  1. data class Person(var name: String, var age: Int, var city: String) {
  2. fun moveTo(newCity: String) { city = newCity }
  3. fun incrementAge() { age++ }
  4. }
  5. fun main() {
  6. //sampleStart
  7. Person("Alice", 20, "Amsterdam").let {
  8. println(it)
  9. it.moveTo("London")
  10. it.incrementAge()
  11. println(it)
  12. }
  13. //sampleEnd
  14. }

如果不使用 let 来写这段代码,就必须引入一个新变量,并在每次使用它时重复其名称。

  1. data class Person(var name: String, var age: Int, var city: String) {
  2. fun moveTo(newCity: String) { city = newCity }
  3. fun incrementAge() { age++ }
  4. }
  5. fun main() {
  6. //sampleStart
  7. val alice = Person("Alice", 20, "Amsterdam")
  8. println(alice)
  9. alice.moveTo("London")
  10. alice.incrementAge()
  11. println(alice)
  12. //sampleEnd
  13. }

作用域函数没有引入任何新的技术,但是它们可以使你的代码更加简洁易读。

由于作用域函数的相似性质,为你的案例选择正确的函数可能有点棘手。选择主要取决于你的意图和项目中使用的一致性。下面我们将详细描述各种作用域函数及其约定用法之间的区别。

区别

由于作用域函数本质上都非常相似,因此了解它们之间的区别很重要。每个作用域函数之间有两个主要区别:

  • 引用上下文对象的方式
  • 返回值

上下文对象:this 还是 it

在作用域函数的 lambda 表达式里,上下文对象可以不使用其实际名称而是使用一个更简短的引用来访问。每个作用域函数都使用以下两种方式之一来访问上下文对象:作为 lambda 表达式的接收者this)或者作为 lambda 表达式的参数(it)。两者都提供了同样的功能,因此我们将针对不同的场景描述两者的优缺点,并提供使用建议。

  1. fun main() {
  2. val str = "Hello"
  3. // this
  4. str.run {
  5. println("The receiver string length: $length")
  6. //println("The receiver string length: ${this.length}") // 和上句效果相同
  7. }
  8. // it
  9. str.let {
  10. println("The receiver string's length is ${it.length}")
  11. }
  12. }

this

runwith 以及 apply 通过关键字 this 引用上下文对象。因此,在它们的 lambda 表达式中可以像在普通的类函数中一样访问上下文对象。在大多数场景,当你访问接收者对象时你可以省略 this,来让你的代码更简短。相对地,如果省略了 this,就很难区分接收者对象的成员及外部对象或函数。因此,对于主要对对象成员进行操作(调用其函数或赋值其属性)的 lambda 表达式,建议将上下文对象作为接收者(this)。

  1. data class Person(var name: String, var age: Int = 0, var city: String = "")
  2. fun main() {
  3. //sampleStart
  4. val adam = Person("Adam").apply {
  5. age = 20 // 和 this.age = 20 或者 adam.age = 20 一样
  6. city = "London"
  7. }
  8. println(adam)
  9. //sampleEnd
  10. }

it

反过来,letalso 将上下文对象作为 lambda 表达式参数。如果没有指定参数名,对象可以用隐式默认名称 it 访问。itthis 简短,带有 it 的表达式通常更容易阅读。然而,当调用对象函数或属性时,不能像 this 这样隐式地访问对象。因此,当上下文对象在作用域中主要用作函数调用中的参数时,使用 it 作为上下文对象会更好。若在代码块中使用多个变量,则 it 也更好。

  1. import kotlin.random.Random
  2. fun writeToLog(message: String) {
  3. println("INFO: $message")
  4. }
  5. fun main() {
  6. //sampleStart
  7. fun getRandomInt(): Int {
  8. return Random.nextInt(100).also {
  9. writeToLog("getRandomInt() generated value $it")
  10. }
  11. }
  12. val i = getRandomInt()
  13. //sampleEnd
  14. }

此外,当将上下文对象作为参数传递时,可以为上下文对象指定在作用域内的自定义名称。

  1. import kotlin.random.Random
  2. fun writeToLog(message: String) {
  3. println("INFO: $message")
  4. }
  5. fun main() {
  6. //sampleStart
  7. fun getRandomInt(): Int {
  8. return Random.nextInt(100).also { value ->
  9. writeToLog("getRandomInt() generated value $value")
  10. }
  11. }
  12. val i = getRandomInt()
  13. //sampleEnd
  14. }

返回值

根据返回结果,作用域函数可以分为以下两类:

  • applyalso 返回上下文对象。
  • letrunwith 返回 lambda 表达式结果.

这两个选项使你可以根据在代码中的后续操作来选择适当的函数。

上下文对象

applyalso 的返回值是上下文对象本身。因此,它们可以作为辅助步骤包含在调用链中:你可以继续在同一个对象上进行链式函数调用。

  1. fun main() {
  2. //sampleStart
  3. val numberList = mutableListOf<Double>()
  4. numberList.also { println("Populating the list") }
  5. .apply {
  6. add(2.71)
  7. add(3.14)
  8. add(1.0)
  9. }
  10. .also { println("Sorting the list") }
  11. .sort()
  12. //sampleEnd
  13. println(numberList)
  14. }

它们还可以用在返回上下文对象的函数的 return 语句中。

  1. import kotlin.random.Random
  2. fun writeToLog(message: String) {
  3. println("INFO: $message")
  4. }
  5. fun main() {
  6. //sampleStart
  7. fun getRandomInt(): Int {
  8. return Random.nextInt(100).also {
  9. writeToLog("getRandomInt() generated value $it")
  10. }
  11. }
  12. val i = getRandomInt()
  13. //sampleEnd
  14. }

Lambda 表达式结果

letrunwith 返回 lambda 表达式的结果。所以,在需要使用其结果给一个变量赋值,或者在需要对其结果进行链式操作等情况下,可以使用它们。

  1. fun main() {
  2. //sampleStart
  3. val numbers = mutableListOf("one", "two", "three")
  4. val countEndsWithE = numbers.run {
  5. add("four")
  6. add("five")
  7. count { it.endsWith("e") }
  8. }
  9. println("There are $countEndsWithE elements that end with e.")
  10. //sampleEnd
  11. }

此外,还可以忽略返回值,仅使用作用域函数为变量创建一个临时作用域。

  1. fun main() {
  2. //sampleStart
  3. val numbers = mutableListOf("one", "two", "three")
  4. with(numbers) {
  5. val firstItem = first()
  6. val lastItem = last()
  7. println("First item: $firstItem, last item: $lastItem")
  8. }
  9. //sampleEnd
  10. }

几个函数

为了帮助你为你的场景选择合适的作用域函数,我们会详细地描述它们并且提供一些使用建议。从技术角度来说,作用域函数在很多场景里是可以互换的,所以这些示例展示了定义通用使用风格的约定用法。

let

上下文对象作为 lambda 表达式的参数(it)来访问。返回值是 lambda 表达式的结果。

let 可用于在调用链的结果上调用一个或多个函数。例如,以下代码打印对集合的两个操作的结果:

  1. fun main() {
  2. //sampleStart
  3. val numbers = mutableListOf("one", "two", "three", "four", "five")
  4. val resultList = numbers.map { it.length }.filter { it > 3 }
  5. println(resultList)
  6. //sampleEnd
  7. }

使用 let,可以写成这样:

  1. fun main() {
  2. //sampleStart
  3. val numbers = mutableListOf("one", "two", "three", "four", "five")
  4. numbers.map { it.length }.filter { it > 3 }.let {
  5. println(it)
  6. // 如果需要可以调用更多函数
  7. }
  8. //sampleEnd
  9. }

若代码块仅包含以 it 作为参数的单个函数,则可以使用方法引用(::)代替 lambda 表达式:

  1. fun main() {
  2. //sampleStart
  3. val numbers = mutableListOf("one", "two", "three", "four", "five")
  4. numbers.map { it.length }.filter { it > 3 }.let(::println)
  5. //sampleEnd
  6. }

let 经常用于仅使用非空值执行代码块。如需对非空对象执行操作,可对其使用安全调用操作符 ?. 并调用 let 在 lambda 表达式中执行操作。

  1. fun processNonNullString(str: String) {}
  2. fun main() {
  3. //sampleStart
  4. val str: String? = "Hello"
  5. //processNonNullString(str) // 编译错误:str 可能为空
  6. val length = str?.let {
  7. println("let() called on $it")
  8. processNonNullString(it) // 编译通过:'it' 在 '?.let { }' 中必不为空
  9. it.length
  10. }
  11. //sampleEnd
  12. }

使用 let 的另一种情况是引入作用域受限的局部变量以提高代码的可读性。如需为上下文对象定义一个新变量,可提供其名称作为 lambda 表达式参数来替默认的 it

  1. fun main() {
  2. //sampleStart
  3. val numbers = listOf("one", "two", "three", "four")
  4. val modifiedFirstItem = numbers.first().let { firstItem ->
  5. println("The first item of the list is '$firstItem'")
  6. if (firstItem.length >= 5) firstItem else "!" + firstItem + "!"
  7. }.toUpperCase()
  8. println("First item after modifications: '$modifiedFirstItem'")
  9. //sampleEnd
  10. }

with

一个非扩展函数:上下文对象作为参数传递,但是在 lambda 表达式内部,它可以作为接收者(this)使用。 返回值是 lambda 表达式结果。

我们建议使用 with 来调用上下文对象上的函数,而不使用 lambda 表达式结果。 在代码中,with 可以理解为“对于这个对象,执行以下操作。

  1. fun main() {
  2. //sampleStart
  3. val numbers = mutableListOf("one", "two", "three")
  4. with(numbers) {
  5. println("'with' is called with argument $this")
  6. println("It contains $size elements")
  7. }
  8. //sampleEnd
  9. }

with 的另一个使用场景是引入一个辅助对象,其属性或函数将用于计算一个值。

  1. fun main() {
  2. //sampleStart
  3. val numbers = mutableListOf("one", "two", "three")
  4. val firstAndLast = with(numbers) {
  5. "The first element is ${first()}," +
  6. " the last element is ${last()}"
  7. }
  8. println(firstAndLast)
  9. //sampleEnd
  10. }

run

上下文对象 作为接收者(this)来访问。 返回值 是 lambda 表达式结果。

runwith 做同样的事情,但是调用方式和 let 一样——作为上下文对象的扩展函数.

当 lambda 表达式同时包含对象初始化和返回值的计算时,run 很有用。

  1. class MultiportService(var url: String, var port: Int) {
  2. fun prepareRequest(): String = "Default request"
  3. fun query(request: String): String = "Result for query '$request'"
  4. }
  5. fun main() {
  6. //sampleStart
  7. val service = MultiportService("https://example.kotlinlang.org", 80)
  8. val result = service.run {
  9. port = 8080
  10. query(prepareRequest() + " to port $port")
  11. }
  12. // 同样的代码如果用 let() 函数来写:
  13. val letResult = service.let {
  14. it.port = 8080
  15. it.query(it.prepareRequest() + " to port ${it.port}")
  16. }
  17. //sampleEnd
  18. println(result)
  19. println(letResult)
  20. }

除了在接收者对象上调用 run 之外,还可以将其用作非扩展函数。 非扩展 run 可以使你在需要表达式的地方执行一个由多个语句组成的块。

  1. fun main() {
  2. //sampleStart
  3. val hexNumberRegex = run {
  4. val digits = "0-9"
  5. val hexDigits = "A-Fa-f"
  6. val sign = "+-"
  7. Regex("[$sign]?[$digits$hexDigits]+")
  8. }
  9. for (match in hexNumberRegex.findAll("+1234 -FFFF not-a-number")) {
  10. println(match.value)
  11. }
  12. //sampleEnd
  13. }

apply

上下文对象 作为接收者(this)来访问。 返回值 是上下文对象本身。

对于不返回值且主要在接收者(this)对象的成员上运行的代码块使用 applyapply 的常见情况是对象配置。这样的调用可以理解为“将以下赋值操作应用于对象”。

  1. data class Person(var name: String, var age: Int = 0, var city: String = "")
  2. fun main() {
  3. //sampleStart
  4. val adam = Person("Adam").apply {
  5. age = 32
  6. city = "London"
  7. }
  8. println(adam)
  9. //sampleEnd
  10. }

将接收者作为返回值,你可以轻松地将 apply 包含到调用链中以进行更复杂的处理。

also

上下文对象作为 lambda 表达式的参数(it)来访问。 返回值是上下文对象本身。

also 对于执行一些将上下文对象作为参数的操作很有用。 对于需要引用对象而不是其属性与函数的操作,或者不想屏蔽来自外部作用域的 this 引用时,请使用 also

当你在代码中看到 also 时,可以将其理解为“并且用该对象执行以下操作”。

  1. fun main() {
  2. //sampleStart
  3. val numbers = mutableListOf("one", "two", "three")
  4. numbers
  5. .also { println("The list elements before adding new one: $it") }
  6. .add("four")
  7. //sampleEnd
  8. }

函数选择

为了帮助你选择合适的作用域函数,我们提供了它们之间的主要区别表。

函数对象引用返回值是否是扩展函数
letitLambda 表达式结果
runthisLambda 表达式结果
run-Lambda 表达式结果不是:调用无需上下文对象
withthisLambda 表达式结果不是:把上下文对象当做参数
applythis上下文对象
alsoit上下文对象

以下是根据预期目的选择作用域函数的简短指南:

  • 对一个非空(non-null)对象执行 lambda 表达式:let
  • 将表达式作为变量引入为局部作用域中:let
  • 对象配置:apply
  • 对象配置并且计算结果:run
  • 在需要表达式的地方运行语句:非扩展的 run
  • 附加效果:also
  • 一个对象的一组函数调用:with

不同函数的使用场景存在重叠,你可以根据项目或团队中使用的特定约定选择函数。

尽管作用域函数是使代码更简洁的一种方法,但请避免过度使用它们:这会降低代码的可读性并可能导致错误。避免嵌套作用域函数,同时链式调用它们时要小心:此时很容易对当前上下文对象及 thisit 的值感到困惑。

takeIftakeUnless

除了作用域函数外,标准库还包含函数 takeIftakeUnless。这俩函数使你可以将对象状态检查嵌入到调用链中。

当以提供的谓词在对象上进行调用时,若该对象与谓词匹配,则 takeIf 返回此对象。否则返回 null。因此,takeIf 是单个对象的过滤函数。反之,takeUnless如果不匹配谓词,则返回对象,如果匹配则返回 null。该对象作为 lambda 表达式参数(it)来访问。

  1. import kotlin.random.*
  2. fun main() {
  3. //sampleStart
  4. val number = Random.nextInt(100)
  5. val evenOrNull = number.takeIf { it % 2 == 0 }
  6. val oddOrNull = number.takeUnless { it % 2 == 0 }
  7. println("even: $evenOrNull, odd: $oddOrNull")
  8. //sampleEnd
  9. }

当在 takeIftakeUnless 之后链式调用其他函数,不要忘记执行空检查或安全调用(?.),因为他们的返回值是可为空的。

  1. fun main() {
  2. //sampleStart
  3. val str = "Hello"
  4. val caps = str.takeIf { it.isNotEmpty() }?.toUpperCase()
  5. //val caps = str.takeIf { it.isNotEmpty() }.toUpperCase() // 编译错误
  6. println(caps)
  7. //sampleEnd
  8. }

takeIftakeUnless 与作用域函数一起特别有用。 一个很好的例子是用 let 链接它们,以便在与给定谓词匹配的对象上运行代码块。 为此,请在对象上调用 takeIf,然后通过安全调用(?.)调用 let。对于与谓词不匹配的对象,takeIf 返回 null,并且不调用 let

  1. fun main() {
  2. //sampleStart
  3. fun displaySubstringPosition(input: String, sub: String) {
  4. input.indexOf(sub).takeIf { it >= 0 }?.let {
  5. println("The substring $sub is found in $input.")
  6. println("Its start position is $it.")
  7. }
  8. }
  9. displaySubstringPosition("010000011", "11")
  10. displaySubstringPosition("010000011", "12")
  11. //sampleEnd
  12. }

没有标准库函数时,相同的函数看起来是这样的:

  1. fun main() {
  2. //sampleStart
  3. fun displaySubstringPosition(input: String, sub: String) {
  4. val index = input.indexOf(sub)
  5. if (index >= 0) {
  6. println("The substring $sub is found in $input.")
  7. println("Its start position is $index.")
  8. }
  9. }
  10. displaySubstringPosition("010000011", "11")
  11. displaySubstringPosition("010000011", "12")
  12. //sampleEnd
  13. }