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)
}
|
示例解答
另请参阅
上一步
下一步