3.3.2 作用域函数

原文链接: https://kotlinlang.org/docs/kotlin-tour-intermediate-scope-functions.html

3.3.2 作用域函数

本章将基于你对扩展函数的理解,学习如何使用作用域函数写出更符合 Kotlin 习惯的代码。

作用域函数

在编程中,作用域是指变量或对象被识别的区域。最常提到的两种作用域是全局作用域和局部作用域:

  • 全局作用域 —— 变量或对象可以在程序的任何位置访问。
  • 局部作用域 —— 变量或对象只能在其定义所在的代码块或函数内部访问。

在 Kotlin 中还有作用域函数,它们允许你围绕某个对象创建一个临时作用域并执行一些代码。

作用域函数让你的代码更简洁,因为你不必在临时作用域内反复引用对象名。根据作用域函数的不同,你可以通过关键字 this 引用该对象,或通过关键字 it 把它当作参数使用。

Kotlin 共有五个作用域函数:let、apply、run、also 和 with。

每个作用域函数都接收一个 lambda 表达式,并返回该对象或 lambda 表达式的结果。本次导览会逐一讲解每个作用域函数及其用法。

提示: 你也可以观看 Kotlin 开发者布道师 Sebastian Aigner 关于作用域函数的演讲 Back to the Stdlib: Making the Most of Kotlin’s Standard Library。

let

当你想在代码中做空值检查,并随后对返回的对象执行进一步操作时,请使用 let 作用域函数。

看下面这个示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
fun sendNotification(recipientAddress: String): String {
    println("Yo $recipientAddress!")
    return "Notification sent!"
}

fun getNextAddress(): String {
    return "sebastian@jetbrains.com"
}

fun main() {
    val address: String? = getNextAddress()
    sendNotification(address)
}

这个示例有两个函数:

  • sendNotification(),它有一个函数参数 recipientAddress,并返回一个字符串。
  • getNextAddress(),它没有函数参数,并返回一个字符串。

示例创建了一个类型为可空 String 的变量 address。但当你调用 sendNotification() 函数时这就成了问题,因为该函数并不认为 address 可能是 null 值。因此编译器会报错:

1
Argument type mismatch: actual type is 'String?', but 'String' was expected.

在入门导览中你已经知道,可以用 if 条件做空值检查,或者使用 Elvis 运算符 ?:。但如果你想在代码后面使用这个返回的对象呢?你可以用 if 条件加上 else 分支来实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
fun sendNotification(recipientAddress: String): String {
    println("Yo $recipientAddress!")
    return "Notification sent!"
}

fun getNextAddress(): String {
    return "sebastian@jetbrains.com"
}

fun main() {
    val address: String? = getNextAddress()
    val confirm = if(address != null) {
        sendNotification(address)
    } else { null }
}

不过,更简洁的方式是使用 let 作用域函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
fun sendNotification(recipientAddress: String): String {
    println("Yo $recipientAddress!")
    return "Notification sent!"
}

fun getNextAddress(): String {
    return "sebastian@jetbrains.com"
}

fun main() {
    val address: String? = getNextAddress()
    val confirm = address?.let {
        sendNotification(it)
    }
}

这个示例:

  • 创建了名为 address 和 confirm 的变量。
  • 在 address 变量上对 let 作用域函数使用安全调用。
  • 在 let 作用域函数内部创建了一个临时作用域。
  • 把 sendNotification() 函数作为 lambda 表达式传入 let 作用域函数。
  • 在这个临时作用域中通过 it 引用 address 变量。
  • 把结果赋给 confirm 变量。

采用这种方式,你的代码就能处理 address 变量可能是 null 值的情况,而且可以在后面继续使用 confirm 变量。

apply

使用 apply 作用域函数可以在创建对象(例如类实例)时初始化它,而不必在代码后面再做。这种方式让代码更易读、更易管理。

看下面这个示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
class Client() {
    var token: String? = null
    fun connect() = println("connected!")
    fun authenticate() = println("authenticated!")
    fun getData() : String {
        println("getting data!")
        return "Mock data"
    }
}

val client = Client()

fun main() {
    client.token = "asdf"
    client.connect()
    // connected!
    client.authenticate()
    // authenticated!
    client.getData()
    // getting data!
}

这个示例有一个 Client 类,其中包含一个名为 token 的属性和三个成员函数:connect()、authenticate() 和 getData()。

示例在 main() 函数中先创建了 client 作为 Client 类的实例,然后初始化它的 token 属性并调用它的成员函数。

虽然这个示例很紧凑,但在实际项目中,创建类实例之后可能要很久才会去配置和使用它(及其成员函数)。而如果使用 apply 作用域函数,你就可以在代码中的同一个位置完成类实例的创建、配置和成员函数的调用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
class Client() {
    var token: String? = null
    fun connect() = println("connected!")
    fun authenticate() = println("authenticated!")
    fun getData() : String {
        println("getting data!")
        return "Mock data"
    }
}
val client = Client().apply {
    token = "asdf"
    connect()
    // connected!
    authenticate()
    // authenticated!
}

fun main() {
    client.getData()
    // getting data!
}

这个示例:

  • 创建了 client 作为 Client 类的实例。
  • 在 client 实例上使用 apply 作用域函数。
  • 在 apply 作用域函数内部创建了一个临时作用域,这样在访问它的属性或函数时就不必显式引用 client 实例。
  • 向 apply 作用域函数传入一个 lambda 表达式,其中更新了 token 属性并调用了 connect() 和 authenticate() 函数。
  • 在 main() 函数中调用 client 实例的 getData() 成员函数。

如你所见,在处理大段代码时,这种策略非常方便。

run

与 apply 类似,你可以用 run 作用域函数初始化对象,但 run 更适合在代码中的某个特定时刻初始化对象,并且立即计算出一个结果。

我们接着上面 apply 的示例来讲,不过这次你希望把 connect() 和 authenticate() 两个函数归为一组,以便每次请求时都一起调用。

例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Client() {
    var token: String? = null
    fun connect() = println("connected!")
    fun authenticate() = println("authenticated!")
    fun getData() : String {
        println("getting data!")
        return "Mock data"
    }
}

val client: Client = Client().apply {
    token = "asdf"
}

fun main() {
    val result: String = client.run {
        connect()
        // connected!
        authenticate()
        // authenticated!
        getData()
        // getting data!
    }
}

这个示例:

  • 创建了 client 作为 Client 类的实例。
  • 在 client 实例上使用 apply 作用域函数。
  • 在 apply 作用域函数内部创建了一个临时作用域,这样在访问它的属性或函数时就不必显式引用 client 实例。
  • 向 apply 作用域函数传入一个 lambda 表达式,其中更新了 token 属性。

main() 函数:

  • 创建了一个类型为 String 的 result 变量。
  • 在 client 实例上使用 run 作用域函数。
  • 在 run 作用域函数内部创建了一个临时作用域,这样在访问它的属性或函数时就不必显式引用 client 实例。
  • 向 run 作用域函数传入一个 lambda 表达式,其中调用了 connect()、authenticate() 和 getData() 函数。
  • 把结果赋给 result 变量。

现在你就可以在代码后面继续使用这个返回的结果了。

also

使用 also 作用域函数可以对对象执行一个附加操作,然后把该对象返回,以便在代码中继续使用它,例如写日志。

看下面这个示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
fun main() {
    val medals: List<String> = listOf("Gold", "Silver", "Bronze")
    val reversedLongUppercaseMedals: List<String> =
        medals
            .map { it.uppercase() }
            .filter { it.length > 4 }
            .reversed()
    println(reversedLongUppercaseMedals)
    // [BRONZE, SILVER]
}

这个示例:

  • 创建了包含字符串列表的 medals 变量。
  • 创建了类型为 List<String> 的 reversedLongUpperCaseMedals 变量。
  • 在 medals 变量上使用 .map() 扩展函数。
  • 向 .map() 函数传入一个 lambda 表达式,它通过 it 关键字引用 medals,并在其上调用 .uppercase() 扩展函数。
  • 在 medals 变量上使用 .filter() 扩展函数。
  • 向 .filter() 函数传入一个 lambda 表达式作为判断条件,它通过 it 关键字引用 medals,并检查列表中的项目是否超过 4 个字符。
  • 在 medals 变量上使用 .reversed() 扩展函数。
  • 把结果赋给 reversedLongUpperCaseMedals 变量。
  • 打印 reversedLongUpperCaseMedals 变量中包含的列表。

在这些函数调用之间加入一些日志、看看 medals 变量发生了什么变化会很有用。also 函数就能帮上忙:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
fun main() {
    val medals: List<String> = listOf("Gold", "Silver", "Bronze")
    val reversedLongUppercaseMedals: List<String> =
        medals
            .map { it.uppercase() }
            .also { println(it) }
            // [GOLD, SILVER, BRONZE]
            .filter { it.length > 4 }
            .also { println(it) }
            // [SILVER, BRONZE]
            .reversed()
    println(reversedLongUppercaseMedals)
    // [BRONZE, SILVER]
}

现在这个示例:

  • 在 medals 变量上使用 also 作用域函数。
  • 在 also 作用域函数内部创建了一个临时作用域,这样在把它当作函数参数使用时就不必显式引用 medals 变量。
  • 向 also 作用域函数传入一个 lambda 表达式,它通过 it 关键字把 medals 变量作为函数参数调用 println() 函数。

由于 also 函数会返回该对象,它不仅适用于写日志,也适用于调试、串联多个操作,以及执行其他不影响代码主流程的副作用操作。

with

与其他作用域函数不同,with 不是扩展函数,因此语法也不一样。你要把接收者对象作为参数传给 with。

当你想在同一个对象上调用多个函数时,请使用 with 作用域函数。

看下面这个示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
class Canvas {
    fun rect(x: Int, y: Int, w: Int, h: Int): Unit = println("$x, $y, $w, $h")
    fun circ(x: Int, y: Int, rad: Int): Unit = println("$x, $y, $rad")
    fun text(x: Int, y: Int, str: String): Unit = println("$x, $y, $str")
}

fun main() {
    val mainMonitorPrimaryBufferBackedCanvas = Canvas()

    mainMonitorPrimaryBufferBackedCanvas.text(10, 10, "Foo")
    mainMonitorPrimaryBufferBackedCanvas.rect(20, 30, 100, 50)
    mainMonitorPrimaryBufferBackedCanvas.circ(40, 60, 25)
    mainMonitorPrimaryBufferBackedCanvas.text(15, 45, "Hello")
    mainMonitorPrimaryBufferBackedCanvas.rect(70, 80, 150, 100)
    mainMonitorPrimaryBufferBackedCanvas.circ(90, 110, 40)
    mainMonitorPrimaryBufferBackedCanvas.text(35, 55, "World")
    mainMonitorPrimaryBufferBackedCanvas.rect(120, 140, 200, 75)
    mainMonitorPrimaryBufferBackedCanvas.circ(160, 180, 55)
    mainMonitorPrimaryBufferBackedCanvas.text(50, 70, "Kotlin")
}

这个示例创建了一个 Canvas 类,它有三个成员函数:rect()、circ() 和 text()。每个成员函数都会打印一条由你提供的函数参数组成的语句。

示例创建了 mainMonitorPrimaryBufferBackedCanvas 作为 Canvas 类的实例,然后在该实例上以不同的函数参数依次调用成员函数。

你可以看出这段代码很难读。如果使用 with 函数,代码就变得简洁了:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
class Canvas {
    fun rect(x: Int, y: Int, w: Int, h: Int): Unit = println("$x, $y, $w, $h")
    fun circ(x: Int, y: Int, rad: Int): Unit = println("$x, $y, $rad")
    fun text(x: Int, y: Int, str: String): Unit = println("$x, $y, $str")
}

fun main() {
    val mainMonitorSecondaryBufferBackedCanvas = Canvas()
    with(mainMonitorSecondaryBufferBackedCanvas) {
        text(10, 10, "Foo")
        rect(20, 30, 100, 50)
        circ(40, 60, 25)
        text(15, 45, "Hello")
        rect(70, 80, 150, 100)
        circ(90, 110, 40)
        text(35, 55, "World")
        rect(120, 140, 200, 75)
        circ(160, 180, 55)
        text(50, 70, "Kotlin")
    }
}

这个示例:

  • 使用 with 作用域函数,并以 mainMonitorSecondaryBufferBackedCanvas 实例作为接收者。
  • 在 with 作用域函数内部创建了一个临时作用域,这样在调用它的成员函数时就不必显式引用 mainMonitorSecondaryBufferBackedCanvas 实例。
  • 向 with 作用域函数传入一个 lambda 表达式,其中以不同的函数参数依次调用一系列成员函数。

现在这段代码易读多了,你也就更不容易出错。

使用场景概览

本节介绍了 Kotlin 中可用的各个作用域函数,以及它们让代码更符合习惯的主要使用场景。你可以把下面这张表当作速查表。需要注意的是,你并不需要完全理解这些函数的内部工作原理,就可以在代码中使用它们。

| 函数 | 通过什么访问 x | 返回值 | 使用场景 | | let | it | lambda 结果 | 在代码中做空值检查,并随后对返回的对象执行进一步操作。 | | apply | this | x | 在创建对象时初始化它。 | | run | this | lambda 结果 | 在创建对象时初始化它,并且计算出一个结果。 | | also | it | x | 在返回对象之前完成附加操作。 | | with | this | lambda 结果 | 在同一个对象上调用多个函数。 |

关于作用域函数的更多信息,请参阅作用域函数。

练习

习题 1

把 .getPriceInEuros() 函数改写为使用安全调用运算符 ?. 和 let 作用域函数的单表达式函数。

提示

使用安全调用运算符 ?. 安全地访问 getProductInfo() 函数返回结果中的 priceInDollars 属性。然后使用 let 作用域函数把 priceInDollars 的值转换成欧元。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
data class ProductInfo(val priceInDollars: Double?)

class Product {
    fun getProductInfo(): ProductInfo? {
        return ProductInfo(100.0)
    }
}

// Rewrite this function
fun Product.getPriceInEuros(): Double? {
    val info = getProductInfo()
    if (info == null) return null
    val price = info.priceInDollars
    if (price == null) return null
    return convertToEuros(price)
}

fun convertToEuros(dollars: Double): Double {
    return dollars * 0.85
}

fun main() {
    val product = Product()
    val priceInEuros = product.getPriceInEuros()

    if (priceInEuros != null) {
        println("Price in Euros: €$priceInEuros")
        // Price in Euros: €85.0
    } else {
        println("Price information is not available.")
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
data class ProductInfo(val priceInDollars: Double?)

class Product {
    fun getProductInfo(): ProductInfo? {
        return ProductInfo(100.0)
    }
}

fun Product.getPriceInEuros() = getProductInfo()?.priceInDollars?.let { convertToEuros(it) }

fun convertToEuros(dollars: Double): Double {
    return dollars * 0.85
}

fun main() {
    val product = Product()
    val priceInEuros = product.getPriceInEuros()

    if (priceInEuros != null) {
        println("Price in Euros: €$priceInEuros")
        // Price in Euros: €85.0
    } else {
        println("Price information is not available.")
    }
}

示例解答

习题 2

你有一个用于更新用户邮箱地址的 updateEmail() 函数。请使用 apply 作用域函数更新邮箱地址,然后用 also 作用域函数打印一条日志消息:Updating email for user with ID: ${it.id}。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
data class User(val id: Int, var email: String)

fun updateEmail(user: User, newEmail: String): User = // Write your code here

fun main() {
    val user = User(1, "old_email@example.com")
    val updatedUser = updateEmail(user, "new_email@example.com")
    // Updating email for user with ID: 1

    println("Updated User: $updatedUser")
    // Updated User: User(id=1, email=new_email@example.com)
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
data class User(val id: Int, var email: String)

fun updateEmail(user: User, newEmail: String): User = user.apply {
    this.email = newEmail
}.also { println("Updating email for user with ID: ${it.id}") }

fun main() {
    val user = User(1, "old_email@example.com")
    val updatedUser = updateEmail(user, "new_email@example.com")
    // Updating email for user with ID: 1

    println("Updated User: $updatedUser")
    // Updated User: User(id=1, email=new_email@example.com)
}

示例解答

另请参阅

上一步

下一步