本文将伴随大家进入Kotlin语言的正式学习生涯中,希望大家不要半途而废哦!笔者将Kotlin用于Android开发中,因此将从Android开发的视角叙述相关内容,同时将与Java语言有所联系。

作用域函数

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

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

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

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

1. let

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

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

1
2
3
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
val numbers = mutableListOf("one", "two", "three", "four", "five")
numbers.map { it.length }.filter { it > 3 }.let {
println(it)
// 如果需要可以调用更多函数
}

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

1
2
val numbers = mutableListOf("one", "two", "three", "four", "five")
numbers.map { it.length }.filter { it > 3 }.let(::println)

let 经常用于仅使用非空值执行代码块。如需对非空对象执行操作,可对其使用安全调用操作符 ?. 并调用 letlambda 表达式中执行操作。这种写法也可以用于判空后执行相应逻辑。

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

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

1
2
3
4
5
6
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 + "!"
}.toUpperCase()
println("First item after modifications: '$modifiedFirstItem'")

2. with

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

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

1
2
3
4
5
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
val numbers = mutableListOf("one", "two", "three")
val firstAndLast = with(numbers) {
"The first element is ${first()}," +
" the last element is ${last()}"
}
println(firstAndLast)

3. run

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
val service = MultiportService("https://example.kotlinlang.org", 80)

val result = service.run {
port = 8080
query(prepareRequest() + " to port $port")
}

// 同样的代码如果用 let() 函数来写:
val letResult = service.let {
it.port = 8080
it.query(it.prepareRequest() + " to port ${it.port}")
}

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

1
2
3
4
5
6
7
8
9
10
11
val hexNumberRegex = run {
val digits = "0-9"
val hexDigits = "A-Fa-f"
val sign = "+-"

Regex("[$sign]?[$digits$hexDigits]+")
}

for (match in hexNumberRegex.findAll("+1234 -FFFF not-a-number")) {
println(match.value)
}

4. apply

apply函数也是把上下文对象作为接收者this来访问。 返回值 是上下文对象本身。

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

1
2
3
4
5
val adam = Person("Adam").apply {
age = 32
city = "London"
}
println(adam)

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

5. also

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

also 对于执行一些将上下文对象作为参数的操作很有用。 对于不会改变上下文对象的操作,可使用 also,例如记录或打印调试信息。 通常,你可以在不破坏程序逻辑的情况下从调用链中删除 also 的调用。

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

1
2
3
4
val numbers = mutableListOf("one", "two", "three")
numbers
.also { println("The list elements before adding new one: $it") }
.add("four")

6. 函数选择

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

函数 对象引用 返回值 是否是扩展函数
let it Lambda 表达式结果
run this Lambda 表达式结果
run - Lambda 表达式结果 不是:调用无需上下文对象
with this Lambda 表达式结果 不是:把上下文对象当做参数
apply this 上下文对象
also it 上下文对象

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

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

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

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