作用域函数
作用域函数 创建了一个临时作用域,您可以在其中访问对象而无需使用其名称。
作用域函数存在的目的仅在于使您的代码更简洁和可读。它们不提供额外的功能。
有五个作用域函数:let()
、run()
、with()
、apply()
和 also()
。它们设计用于与 lambda 一起工作,不需要 import
。它们在访问 上下文对象 方面存在差异,使用 it
或 this
,以及它们返回的内容。with()
使用与其他作用域函数不同的调用语法。下面您可以看到它们之间的区别:
// ScopeFunctions/Differences.kt
package scopefunctions
import atomictest.eq
data class Tag(var n: Int = 0) {
var s: String = ""
fun increment() = ++n
}
fun main() {
// let(): 使用 'it' 访问对象
// 返回 lambda 中的最后一个表达式的结果
Tag(1).let {
it.s = "let: ${it.n}"
it.increment()
} eq 2
// 带有命名 lambda 参数的 let():
Tag(2).let { tag ->
tag.s = "let: ${tag.n}"
tag.increment()
} eq 3
// run(): 使用 'this' 访问对象
// 返回 lambda 中的最后一个表达式的结果
Tag(3).run {
s = "run: $n" // 隐式 'this'
increment() // 隐式 'this'
} eq 4
// with(): 使用 'this' 访问对象
// 返回 lambda 中的最后一个表达式的结果
with(Tag(4)) {
s = "with: $n"
increment()
} eq 5
// apply(): 使用 'this' 访问对象
// 返回修改后的对象
Tag(5).apply {
s = "apply: $n"
increment()
} eq "Tag(n=6)"
// also(): 使用 'it' 访问对象
// 返回修改后的对象
Tag(6).also {
it.s = "also: ${it.n}"
it.increment()
} eq "Tag(n=7)"
// 带有命名 lambda 参数的 also():
Tag(7).also { tag ->
tag.s = "also: ${tag.n}"
tag.increment()
} eq "Tag(n=8)"
}
有多个作用域函数是因为它们满足不同的需求组合:
- 使用
this
访问上下文对象的作用域函数(run()
、with()
和apply()
)在其作用域块中产生最干净的语法。 - 使用
it
访问上下文对象的作用域函数(let()
和also()
)允许您提供命名的 lambda 参数。 - 返回其 lambda 中的最后一个表达式的作用域函数(
let()
、run()
和with()
)用于创建结果。 - 返回修改后的上下文对象的作用域函数(
apply()
和also()
)用于将表达式链接在一起。
run()
是一个常规函数,而 with()
是一个扩展函数;除此之外,它们是相同的。对于调用链和接收者可为空的情况,优先选择 run()
。
下面是作用域函数特性的总结:
this 上下文 | it 上下文 | |
---|---|---|
生成最后一个表达式的结果 | with 、run | let |
生成接收者 | apply | also |
您可以使用 安全访问运算符 ?.
将作用域函数应用于可空接收者,只有在接收者不为 null
时才会调用作用域函数:
// ScopeFunctions/AndNullability.kt
package scopefunctions
import atomictest.eq
import kotlin.random.Random
fun gets(): String? =
if (Random.nextBoolean()) "str!" else null
fun main() {
gets()?.let {
it.removeSuffix("!") + it.length
}?.eq("str4")
}
在 main()
中,如果 gets()
产生非空结果,则会调用 let
。let
的非空接收者变为 lambda 内部的非空 it
。
将安全访问运算符应用于上下文对象会对整个作用域进行 null
检查,如下所示的 [1]-[4]。否则,在作用域内的每个调用都必须单独进行 null
检查:
// ScopeFunctions/Gnome.kt
package scopefunctions
class Gnome(val name: String) {
fun who() = "Gnome: $name"
}
fun whatGnome(gnome: Gnome?) {
gnome?.let { it.who() } // [1]
gnome.let { it?.who() }
gnome?.run { who() } // [2]
gnome.run { this?.who() }
gnome?.apply { who() } // [3]
gnome.apply { this?.who() }
gnome?.also { it.who() } // [4]
gnome.also { it?.who() }
// 对于 nullability 没有帮助:
with(gnome) { this?.who() }
}
当在 let()
、run()
、apply()
或 also()
上使用安全访问运算符时,如果上下文对象为 null
,则整个作用域都将被忽略:
// ScopeFunctions/NullGnome.kt
package scopefunctions
import atomictest.*
fun whichGnome(gnome: Gnome?) {
trace(gnome?.name)
gnome?.let { trace(it.who()) }
gnome?.run { trace(who()) }
gnome?.apply { trace(who()) }
gnome?.also { trace(it.who()) }
}
fun main() {
whichGnome(Gnome("Bob"))
whichGnome(null)
trace eq """
Bob
Gnome: Bob
Gnome: Bob
Gnome: Bob
Gnome: Bob
null
"""
}
trace
显示,当 whichGnome()
接收到 null
参数时,没有作用域函数会执行。
尝试从 Map
中检索对象会产生可空的结果,因为没有保证它会找到该键的条目。在下面的示例中,我们展示了不同作用域函数应用于 Map
查找结果的情况:
// ScopeFunctions/MapLookup.kt
package scopefunctions
import atomictest.*
data class Plumbus(var id: Int)
fun display(map: Map<String, Plumbus>) {
trace("displaying $map")
val pb1: Plumbus = map["main"]?.let {
it.id += 10
it
} ?: return
trace(pb1)
val pb2: Plumbus? = map["main"]?.run {
id += 9
this
}
trace(pb2)
val pb3: Plumbus? = map["main"]?.apply {
id += 8
}
trace(pb3)
val pb4: Plumbus? = map["main"]?.also {
it.id += 7
}
trace(pb4)
}
fun main() {
display(mapOf("main" to Plumbus(1)))
display(mapOf("none" to Plumbus(2)))
trace eq """
displaying {main=Plumbus(id=1)}
Plumbus(id=11)
Plumbus(id=20)
Plumbus(id=28)
Plumbus(id=35)
displaying {none=Plumbus(id=2)}
"""
}
尽管 with()
可以在此示例中强制使用,但结果太难看,难以考虑。
在 trace
中,您可以看到在第一次调用 display()
时创建了每个 Plumbus
对象,但在第二次调用中没有创建任何对象。查看 pb1
的定义并回想起 Elvis 操作符。如果 ?:
左侧的表达式不为 null
,则它变为结果并赋值给 pb1
。但是,如果该表达式为 null
,则 ?:
的右侧变为结果,即 return
,因此在完成初始化 pb1
之前,display()
返回,因此 pb1
-pb4
的任何值都不会创建。
在可链式调用中,作用域函数可以与可空类型一起使用:
// ScopeFunctions/NameTag.kt
package scopefunctions
import atomictest.trace
val functions = listOf(
fun(name: String?) {
name
?.takeUnless { it.isBlank() }
?.let { trace("$it in let") }
},
fun(name: String?) {
name
?.takeUnless { it.isBlank() }
?.run { trace("$this in run") }
},
fun(name: String?) {
name
?.takeUnless { it.isBlank() }
?.apply { trace("$this in apply") }
},
fun(name: String?) {
name
?.takeUnless { it.isBlank() }
?.also { trace("$it in also") }
},
)
fun main() {
functions.forEach { it(null) }
functions.forEach { it(" ") }
functions.forEach { it("Yumyulack") }
trace eq """
Yumyulack in let
Yumyulack in run
Yumyulack in apply
Yumyulack in also
"""
}
functions
是一个函数引用的 List
,由 main()
中的 forEach
调用应用,使用 it
与函数调用语法。functions
中的每个函数都使用不同的作用域函数。对于 null
或空白输入,forEach
调用 it(null)
和 it(" ")
会被忽略。
在嵌套作用域函数时,在给定上下文中可能会有多个 this
或 it
对象可用。有时很难知道选择了哪个对象:
// ScopeFunctions/Nesting.kt
package scopefunctions
import atomictest.eq
fun nesting(s: String, i: Int): String =
with(s) {
with(i) {
toString()
}
} +
s.let {
i.let {
it.toString()
}
} +
s.run {
i.run {
toString()
}
} +
s.apply {
i.apply {
toString()
}
} +
s.also {
i.also {
it.toString()
}
}
fun main() {
nesting("X", 7) eq "777XX"
}
在所有情况下,对 toString()
的调用都是应用于 Int
,因为最近的隐式 this
或 it
是 Int
。apply()
和 also()
返回修改后的对象 s
,而不是计算结果。由于作用域函数旨在提高可读性,因此嵌套作用域函数是一个值得怀疑的做法。
没有作用域函数提供类似于 use()
的 资源清理 功能:
// ScopeFunctions/Blob.kt
package scopefunctions
import atomictest.*
data class Blob(val id: Int) : AutoCloseable {
override fun toString() = "Blob($id)"
fun show() { trace("$this")}
override fun close() = trace("Close $this")
}
fun main() {
Blob(1).let { it.show() }
Blob(2).run { show() }
with(Blob(3)) { show() }
Blob(4).apply { show() }
Blob(5).also { it.show() }
Blob(6).use { it.show() }
Blob(7).use { it.run { show() } }
Blob(8).apply { show() }.also { it.close() }
Blob(9).also { it.show() }.apply { close() }
Blob(10).apply { show() }.use { }
trace eq """
Blob(1)
Blob(
2)
Blob(3)
Blob(4)
Blob(5)
Blob(6)
Close Blob(6)
Blob(7)
Close Blob(7)
Blob(8)
Close Blob(8)
Blob(9)
Close Blob(9)
Blob(10)
Close Blob(10)
"""
}
尽管 use()
在视觉上与 let()
和 also()
相似,但 use()
不允许从其 lambda 中返回任何内容。这防止了表达式链接或生成结果。
没有 use()
,对于任何作用域函数,close()
都不会被调用。要使用作用域函数并保证清理,将作用域函数置于 use()
lambda 内部,如 Blob(7)
所示。Blob(8)
和 Blob(9)
展示了如何显式调用 close()
,以及如何交替使用 apply()
和 also()
。
Blob(10)
使用 apply()
,结果传递给 use()
,在其 lambda 结束时调用 close()
。
作用域函数是内联的
通常,将 lambda 作为参数传递会将 lambda 代码存储在辅助对象中,与常规函数调用相比,会增加一些运行时开销,但考虑到 lambda 的好处(可读性和代码结构),这种开销通常不是一个问题。此外,JVM 中包含许多优化,通常可以弥补开销。
不管开销有多小,只要有运行时开销,无论多么小,都会产生“谨慎使用某个功能”的建议。通过将作用域函数定义为 inline
,可以消除所有的运行时开销。这样,就可以毫不犹豫地使用作用域函数。
当编译器看到 inline
函数调用时,它将函数体替换为函数调用,将所有参数替换为实际参数。
内联对于小型函数效果很好,其中函数调用的开销可能是整个调用的重要部分。随着函数变得越来越大,与整个调用所需的时间相比,调用的成本会缩小,从而降低了内联的价值。与此同时,生成的字节码会增加,因为在每个调用点都插入了整个函数体。
当内联函数接受一个 lambda 参数时,编译器将 lambda 体与函数体一起内联。因此,在将 lambda 直接调用或传递给另一个 inline
函数时,不会创建其他类或对象来传递 lambda。 (这仅在直接调用 lambda 或传递给另一个 inline
函数时才有效)。
尽管可以将其应用于任何函数,但 inline
用于内联 lambda 体或创建 具体化泛型 是有意义的。您可以在 这里 找到有关内联函数的更多信息。
练习和解答可在 www.AtomicKotlin.com 找到。