作用域函数
Kotlin 标准库包含几个函数,它们的唯一目的是在对象的上下文中执行代码块。当对一个对象调用这样的函数 并提供一个 lambda 表达式时,它会形成一个临时作用域。在此作用域中,可以访问该对象而无需其名称。这些 函数称为作用域函数。共有以下五种:let 、run 、with 、apply 以及 also
这些函数基本上做了同样的事情:在一个对象上执行一个代码块。不同的是这个对象在块中如何使用,以及整个 表达式的结果是什么
下面是作用域函数的典型用法
Person("Alice", 20, "Amsterdam").let { println(it) it.moveTo("London") it.incrementAge() println(it) }
如果不使用 let 来写这段代码,就必须引入一个新变量,并在每次使用它时重复其名称
val alice = Person("Alice", 20, "Amsterdam") println(alice) alice.moveTo("London") alice.incrementAge() println(alice)
作用域函数没有引入任何新的技术,但是它们可以使你的代码更加简洁易读。
由于作用域函数的相似性质,为你的案例选择正确的函数可能有点棘手。选择主要取决于你的意图和项目中使 用的一致性。下面我们将详细描述各种作用域函数及其约定用法之间的区别
区别
由于作用域函数本质上都非常相似,因此了解它们之间的区别很重要。每个作用域函数之间有两个主要区别:
— 引用上下文对象的方式
— 返回值
上下文对象:this 还是 it
在作用域函数的 lambda 表达式里,上下文对象可以不使用其实际名称而是使用一个更简短的引用来访问。每 个作用域函数都使用以下两种方式之一来访问上下文对象:作为 lambda 表达式的接收者( this )或者作为 lambda 表达式的参数( it )。两者都提供了同样的功能,因此我们将针对不同的场景描述两者的优缺点,并提供使用建议
fun main() { val str = "Hello" // this str.run { println("The receiver string length: $length") //println("The receiver string length: ${this.length}") // 和上句效果相同 } // it str.let { println("The receiver string's length is ${it.length}") } }
run 、with 以及 apply 通过关键字 this 引用上下文对象。因此,在它们的 lambda 表达式中可以像在普 通的类函数中一样访问上下文对象。在大多数场景,当你访问接收者对象时你可以省略 this,来让你的代码更 简短。相对地,如果省略了 this,就很难区分接收者对象的成员及外部对象或函数。因此,对于主要对对象成员 进行操作(调用其函数或赋值其属性)的 lambda 表达式,建议将上下文对象作为接收者( this )
val adam = Person("Adam").apply { age = 20 // 和 this.age = 20 或者 adam.age = 20 一样 city = "London" } println(adam)
反过来,let 及 also 将上下文对象作为 lambda 表达式参数。如果没有指定参数名,对象可以用隐式默认名 称 it 访问。it 比 this 简短,带有 it 的表达式通常更容易阅读。然而,当调用对象函数或属性时,不能像this 这样隐式地访问对象。因此,当上下文对象在作用域中主要用作函数调用中的参数时,使用 it 作为上下 文对象会更好。若在代码块中使用多个变量,则 it 也更好
fun getRandomInt(): Int { return Random.nextInt(100).also { writeToLog("getRandomInt() generated value $it") } } val i = getRandomInt()
此外,当将上下文对象作为参数传递时,可以为上下文对象指定在作用域内的自定义名称
fun getRandomInt(): Int { return Random.nextInt(100).also { value -> writeToLog("getRandomInt() generated value $value") } } val i = getRandomInt()
返回值
根据返回结果,作用域函数可以分为以下两类
— apply 及 also 返回上下文对象。
— let 、run 及 with 返回 lambda 表达式结果.
这两个选项使你可以根据在代码中的后续操作来选择适当的函数。
apply 及 also 的返回值是上下文对象本身。因此,它们可以作为辅助步骤包含在调用链中:你可以继续在同 一个对象上进行链式函数调用
val numberList = mutableListOf<Double>() numberList.also { println("Populating the list") } .apply { add(2.71) add(3.14) add(1.0) } .also { println("Sorting the list") } .sort()
它们还可以用在返回上下文对象的函数的 return 语句中
fun getRandomInt(): Int { return Random.nextInt(100).also { writeToLog("getRandomInt() generated value $it") } } val i = getRandomInt()
let 、run 及 with 返回 lambda 表达式的结果。所以,在需要使用其结果给一个变量赋值,或者在需要对其结果进行链式操作等情况下,可以使用它们
val numbers = mutableListOf("one", "two", "three") val countEndsWithE = numbers.run { add("four") add("five") count { it.endsWith("e") } } println("There are $countEndsWithE elements that end with e.")
此外,还可以忽略返回值,仅使用作用域函数为变量创建一个临时作用域。
val numbers = mutableListOf("one", "two", "three") with(numbers) { val firstItem = first() val lastItem = last() println("First item: $firstItem, last item: $lastItem") }
几个函数
为了帮助你为你的场景选择合适的作用域函数,我们会详细地描述它们并且提供一些使用建议。从技术⻆度来 说,作用域函数在很多场景里是可以互换的,所以这些示例展示了定义通用使用⻛格的约定用法
let
上下文对象作为 lambda 表达式的参数( it )来访问。返回值是 lambda 表达式的结果。
let 可用于在调用链的结果上调用一个或多个函数。例如,以下代码打印对集合的两个操作的结果
val numbers = mutableListOf("one", "two", "three", "four", "five") val resultList = numbers.map { it.length }.filter { it > 3 } println(resultList)
使用 let,可以写成这样
val numbers = mutableListOf("one", "two", "three", "four", "five") numbers.map { it.length }.filter { it > 3 }.let { println(it) // 如果需要可以调用更多函数 }
若代码块仅包含以 it 作为参数的单个函数,则可以使用方法引用( :: )代替 lambda 表达式
val numbers = mutableListOf("one", "two", "three", "four", "five") numbers.map { it.length }.filter { it > 3 }.let(::println)
let 经常用于仅使用非空值执行代码块。如需对非空对象执行操作,可对其使用安全调用操作符 ?. 并调用 let 在 lambda 表达式中执行操作
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
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'")