Kotlin的作用域函数[译]
Kotlin的作用域函数的介绍
本文时对kotlin官方文档的翻译,文档地址
The Kotlin standard library contains several functions whose sole purpose is to execute a block of code within the context of an object. When you call such a function on an object with a lambda expression provided, it forms a temporary scope. In this scope, you can access the object without its name. Such functions are called scope functions. There are five of them:
let
,run
,with
,apply
, andalso
.Basically, these functions do the same: execute a block of code on an object. What’s different is how this object becomes available inside the block and what is the result of the whole expression.
kotlin的官方标准库包含了几个函数,这些函数的作用是在对象的上下文内部执行代码块。当你在提供了lambda表达式的对象中调用这些方法时,这将形成一个临时的作用域。在这个作用域中,你可以访问当前对象而不使用它的变量名。这些方法被称为作用域函数。这里共有5个作用域函数,分别为:let
,run
,with
,apply
,also
。
基本上,这些方法都做了相同的事情:在对象上执行代码块。不同之处在于这个对象在代码块内部如何变得可用以及整个表达式的结果是什么。
Here’s a typical usage of a scope function:
这里是一个典型的作用域函数使用方式:
data class Person(var name: String, var age: Int, var city: String) {
fun moveTo(newCity: String) { city = newCity }
fun incrementAge() { age++ }
}
fun main() {
//sampleStart
Person("Alice", 20, "Amsterdam").let {
println(it)
it.moveTo("London")
it.incrementAge()
println(it)
}
//sampleEnd
}
If you write the same without
let
, you’ll have to introduce a new variable and repeat its name whenever you use it.
如果你不使用let
这个作用域函数,你需要定义一个变量并且在每个使用到的地方重复它的名字。
data class Person(var name: String, var age: Int, var city: String) {
fun moveTo(newCity: String) { city = newCity }
fun incrementAge() { age++ }
}
fun main() {
//sampleStart
val alice = Person("Alice", 20, "Amsterdam")
println(alice)
alice.moveTo("London")
alice.incrementAge()
println(alice)
//sampleEnd
}
The scope functions do not introduce any new technical capabilities, but they can make your code more concise and readable.
作用域函数不需要引入任何其他的技术能力,但可以使代码更简明易懂。
Due to the similar nature of scope functions, choosing the right one for your case can be a bit tricky. The choice mainly depends on your intent and the consistency of use in your project. Below we’ll provide detailed descriptions of the distinctions between scope functions and the conventions on their usage.
由于作用域函数的相似性,选择一个合适的作用域函数是一件不容易的事情。这个选择主要依赖于你的意图和在项目中的合理性。下面,我们将详细提供关于作用域函数之间的不同和使用惯例的不同。
作用域函数的区别
Because the scope functions are all quite similar in nature, it’s important to understand the differences between them. There are two main differences between each scope function:
- The way to refer to the context object
- The return value
由于作用域函数在性质上非常相似,所以明白他们之间的不同非常重要,他们之间有两个主要的不同。
引用上下文对象的方式
返回值
上下文对象是this还是it
Inside the lambda of a scope function, the context object is available by a short reference instead of its actual name. Each scope function uses one of two ways to access the context object: as a lambda receiver (
this
) or as a lambda argument (it
). Both provide the same capabilities, so we’ll describe the pros and cons of each for different cases and provide recommendations on their use.
在作用域函数的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}") // does the same
}
// it
str.let {
println("The receiver string's length is ${it.length}")
}
}
this
run
,with
, andapply
refer to the context object as a lambda receiver - by keywordthis
. Hence, in their lambdas, the object is available as it would be in ordinary class functions. In most cases, you can omitthis
when accessing the members of the receiver object, making the code shorter. On the other hand, ifthis
is omitted, it can be hard to distinguish between the receiver members and external objects or functions. So, having the context object as a receiver (this
) is recommended for lambdas that mainly operate on the object members: call its functions or assign properties.
run
,with
,apply
通过this
关键字引用到上下文对象作为一个lambda接收者。因此,在这些lambda中,该上下文对象就像在普通类方法中一样可用的。在多数情况下,当你访问接收对象的成员时可以忽略this
关键字,使得代码更短。另一方面,如果this
被省略,这会很难区分接收者的成员和外部对象或方法。因此,更推荐持有上下文对象作为主要操作对象成员的lambda表达式的接收者(this
):调用该对象的方法或指定属性。
data class Person(var name: String, var age: Int = 0, var city: String = "")
fun main() {
//sampleStart
val adam = Person("Adam").apply {
age = 20 // same as this.age = 20 or adam.age = 20
city = "London"
}
//sampleEnd
}
it
In turn,
let
andalso
have the context object as a lambda argument. If the argument name is not specified, the object is accessed by the implicit default nameit
.it
is shorter thanthis
and expressions withit
are usually easier for reading. However, when calling the object functions or properties you don’t have the object available implicitly likethis
. Hence, having the context object asit
is better when the object is mostly used as an argument in function calls.it
is also better if you use multiple variables in the code block.
相反的,let
和also
持有上下文对象作为lambda表达式的参数。如果参数名称是非指定的,那么这个对象就使用隐式的默认名称it
访问。it
比this
更短,并且使用it
的表达式通常更容易阅读。但是,当调用对象的方法或属性时你没有像this
方式一样的隐式可用对象。因此,当主要使用对象作为方法调用中的参数时,最好使用it
的方式持有上下文对象。同样如果在代码块中多次使用变量使用it
也是更好的方式。
import kotlin.random.Random
fun writeToLog(message: String) {
println("INFO: $message")
}
fun main() {
//sampleStart
fun getRandomInt(): Int {
return Random.nextInt(100).also {
writeToLog("getRandomInt() generated value $it")
}
}
val i = getRandomInt()
//sampleEnd
}
Additionally, when you pass the context object as an argument, you can provide a custom name for the context object inside the scope.
另外,当你将上下文对象作为参数传递时,你可以为该对象提供一个在当前作用域内使用的自定义名称。
import kotlin.random.Random
fun writeToLog(message: String) {
println("INFO: $message")
}
fun main() {
//sampleStart
fun getRandomInt(): Int {
return Random.nextInt(100).also { value ->
writeToLog("getRandomInt() generated value $value")
}
}
val i = getRandomInt()
//sampleEnd
}
返回值
The scope functions differ by the result they return
apply
andalso
return the context object .
let
,run
, andwith
return the lambda result .These two options let you choose the proper function depending on what you do next in your code.
作用域函数的不同在于返回结果的不同。
apply
和also
返回上下文对象let
,run
,with
返回lambda的结果
根据你在后续代码中的行为从两个选项中合适的方法。
上下文对象
The return value of
apply
andalso
is the context object itself. Hence, they can be included into call chains as side steps: you can continue chaining function calls on the same object after them.
apply
和alse
的返回值是上下文对象自身。因此,他们可以被包括到调用链中作为连续步骤:你可以在同一个对象上继续链接方法调用。
fun main() {
//sampleStart
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()
//sampleEnd
println(numberList)
}
They also can be used in return statements of functions returning the context object.
他们同样可以被应用在返回上下文对象的方法return语句中。
import kotlin.random.Random
fun writeToLog(message: String) {
println("INFO: $message")
}
fun main() {
//sampleStart
fun getRandomInt(): Int {
return Random.nextInt(100).also {
writeToLog("getRandomInt() generated value $it")
}
}
val i = getRandomInt()
//sampleEnd
}
Lambda表达式结果
let
,run
, andwith
return the lambda result. So, you can use them when assigning the result to a variable, chaining operations on the result, and so on.
let
,run
和with
都返回lambda的结果。因此,你可以在需要将结果赋值给变量,对结果继续链接操作等等情况下选择这三种作用域函数。
fun main() {
//sampleStart
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.")
//sampleEnd
}
Additionally, you can ignore the return value and use a scope function to create a temporary scope for variables.
另外,你可以忽略返回值并且使用作用域函数为变量创建临时作用域。
fun main() {
//sampleStart
val numbers = mutableListOf("one", "two", "three")
with(numbers) {
val firstItem = first()
val lastItem = last()
println("First item: $firstItem, last item: $lastItem")
}
//sampleEnd
}
函数介绍
To help you choose the right scope function for your case, we’ll describe them in detail and provide usage recommendations. Technically, functions are interchangeable in many cases, so the examples show the conventions that define the common usage style.
为了帮助你选择正确的作用域函数,我们将详细描述他们并且提供使用建议。从技术上讲,在许多情况下作用域函数是互通的,因此下面的例子展示了约定俗成的默认的常用使用方式。
let
The context object is available as an argument (
it
). The return value is the lambda result.
let
can be used to invoke one or more functions on results of call chains. For example, the following code prints the results of two operations on a collection:
上下文对象可用作参数(it
),返回值是lambda表达式的结果。
let
可以用于在调用链的结果上调用一个或多个方法。例如,下面的代码打印了一个集合在两个操作后的结果。
fun main() {
//sampleStart
val numbers = mutableListOf("one", "two", "three", "four", "five")
val resultList = numbers.map { it.length }.filter { it > 3 }
println(resultList)
//sampleEnd
}
使用let
,你可以重写成这个形式:
fun main() {
//sampleStart
val numbers = mutableListOf("one", "two", "three", "four", "five")
numbers.map { it.length }.filter { it > 3 }.let {
println(it)
// and more function calls if needed
// 需要的话可以调用其他方法
}
//sampleEnd
}
If the code block contains a single function with
it
as an argument, you can use the method reference (::
) instead of the lambda:
如果代码块中只包含单个方法并且使用了it
作为参数,那么你可以使用方法引用(::)
替代lambda表达式。
fun main() {
//sampleStart
val numbers = mutableListOf("one", "two", "three", "four", "five")
numbers.map { it.length }.filter { it > 3 }.let(::println)
//等效于.let{ println(it) }
//sampleEnd
}
let
is often used for executing a code block only with non-null values. To perform actions on a non-null object, use the safe call operator?.
on it and calllet
with the actions in its lambda.
let
通常用于执行仅使用非空值的代码块。为了对非空对象执行操作,应该使用安全操作符?.
并且调用let
在lambda中执行操作。
fun processNonNullString(str: String) {}
fun main() {
//sampleStart
val str: String? = "Hello"
//processNonNullString(str) // compilation error: str can be null
val length = str?.let {
// 因为使用了?.操作符,当str为null时不会执行到lambda代码块的内容,
// 保证了processNonNullString()方法不会传入空值
println("let() called on $it")
processNonNullString(it) // OK: 'it' is not null inside '?.let { }'
it.length
}
//sampleEnd
}
Another case for using
let
is introducing local variables with a limited scope for improving code readability. To define a new variable for the context object, provide its name as the lambda argument so that it can be used instead of the defaultit
.
另一个使用let
的案例是在有限的作用域内引入局部变量以提高代码可读性。为了给上下文对象定义一个新变量,为lambda提供一个名称作为参数这样可以使用新名称而不是默认的it
。
with
A non-extension function: the context object is passed as an argument, but inside the lambda, it’s available as a receiver (
this
). The return value is the lambda result.We recommend
with
for calling functions on the context object without providing the lambda result. In the code,with
can be read as “with this object, do the following.”
一个非拓展方法:上下文对象是作为参数传递的,但是在lambda内部,它作为一个接收者(this
)。返回值是lambda的结果。
我们推荐在不需要提供lambda结果的情况下使用with
在上下文对象上调用函数。在代码中with
可以解释为“在这个对象上执行如下操作”
fun main() {
//sampleStart
val numbers = mutableListOf("one", "two", "three")
with(numbers) {
println("'with' is called with argument $this")
println("It contains $size elements")
}
//sampleEnd
}
Another use case for
with
is introducing a helper object whose properties or functions will be used for calculating a value.
另一个使用with
的案例是引入一个辅助对象其属性和方法可以用于计算值。
fun main() {
//sampleStart
val numbers = mutableListOf("one", "two", "three")
val firstAndLast = with(numbers) {
"The first element is ${first()}," +
" the last element is ${last()}"
}
println(firstAndLast)
//sampleEnd
}
run
The context object is available as a receiver (
this
). The return value is the lambda result.
run
does the same aswith
but invokes aslet
- as an extension function of the context object.
run
is useful when your lambda contains both the object initialization and the computation of the return value.
上下文对象可用作接收者(this
),返回值是lambda结果。
run
和with
有相同功能,按照let
的调用方式 -作为上下文对象的拓展方法。
run
很有效当你的lambda表达式中都包含了对象初始化和返回值的计算。
class MultiportService(var url: String, var port: Int) {
fun prepareRequest(): String = "Default request"
fun query(request: String): String = "Result for query '$request'"
}
fun main() {
//sampleStart
val service = MultiportService("https://example.kotlinlang.org", 80)
val result = service.run {
port = 8080
query(prepareRequest() + " to port $port")
}
// the same code written with let() function:
val letResult = service.let {
it.port = 8080
it.query(it.prepareRequest() + " to port ${it.port}")
}
//sampleEnd
println(result)
println(letResult)
}
Besides calling
run
on a receiver object, you can use it as a non-extension function. Non-extensionrun
lets you execute a block of several statements where an expression is required.
除了在接收者对象上调用run
方法之外,你还可以使用它作为非拓展方法。非拓展的run
使你可以在需要表达式时执行一个包含多个语句的代码块。
fun main() {
//sampleStart
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)
}
//sampleEnd
}
apply
The context object is available as a receiver (
this
). The return value is the object itself.Use
apply
for code blocks that don’t return a value and mainly operate on the members of the receiver object. The common case forapply
is the object configuration. Such calls can be read as “apply the following assignments to the object.”
上下文对象作为接收者(this
),返回值是上下文对象本身。
apply
用于不返回值并且主要操作于接受对象的成员的代码块,常见的apply
使用场景是对象的配置。apply
方法可以解释为“应用如下的分配到对象”
data class Person(var name: String, var age: Int = 0, var city: String = "")
fun main() {
//sampleStart
val adam = Person("Adam").apply {
age = 32
city = "London"
}
//sampleEnd
}
Having the receiver as the return value, you can easily include
apply
into call chains for more complex processing.
持有接收者作为返回值,你可以更容易地把apply
包括到调用链中以实现更复杂的处理。
also
The context object is available as an argument (
it
). The return value is the object itself.
also
is good for performing some actions that take the context object as an argument. Usealso
for additional actions that don’t alter the object, such as logging or printing debug information. Usually, you can remove the calls ofalso
from the call chain without breaking the program logic.When you see
also
in the code, you can read it as “and also do the following”.
上下文对象作为参数(it
),返回值是上下文对象自身。
also
对一些将上下文对象作为参数传递的行为很有用。对于其他不改变上下文对象例如log或者打印调试信息的行为也可以使用also
。通常,你可以从调用链中移除also
调用而不影响原有程序逻辑。
当你在代码中看到also
时,你可以将其解释为“并且执行如下操作”
fun main() {
//sampleStart
val numbers = mutableListOf("one", "two", "three")
numbers
.also { println("The list elements before adding new one: $it") }
.add("four")
//sampleEnd
}
函数选择
To help you choose the right scope function for your purpose, we provide the table of key differences between them.
为了帮助你选择正确的作用域函数,我们提供了他们的主要区别表
方法名 | 对象引用 | 返回值 | 是否拓展函数 |
---|---|---|---|
let | it | Lambda结果 | 是 |
run | this | Lambda结果 | 是 |
run | - | Lambda结果 | 否:无上下文对象调用 |
with | this | Lambda结果 | 否:上下文对象作为参数传递 |
apply | this | Context object | 是 |
also | it | Context object | 是 |
Here is a short guide for choosing scope functions depending on the intended purpose:
- Executing a lambda on non-null objects:
let
- Introducing an expression as a variable in local scope:
let
- Object configuration:
apply
- Object configuration and computing the result:
run
- Running statements where an expression is required: non-extension
run
- Additional effects:
also
- Grouping function calls on an object:
with
The use cases of different functions overlap, so that you can choose the functions based on the specific conventions used in your project or team.
Although the scope functions are a way of making the code more concise, avoid overusing them: it can decrease your code readability and lead to errors. Avoid nesting scope functions and be careful when chaining them: it’s easy to get confused about the current context object and the value of
this
orit
.
如下是根据预期目的选择合适作用域函数的简单指南。
- 在一个非空对象上执行一个lambda表达式:
let
- 引入一个表达式作为局部作用域中的变量:
let
- 对象配置:
apply
- 对象配置并且计算结果:
run
- 需要表达式的运行语句:非扩展的
run
- 附加效果:
also
- 一个对象上的一组方法调用:
with
不同方法之间的使用场景存在重叠的情况,所以你可以根据你的项目或团队指定的使用约定来选择合适的作用域函数。
虽然作用域函数是使代码更简洁的方法,但应该避免过度使用它们:这会降低代码的可读性并容易导致错误。避免嵌套使用作用域函数并且在链式调用时要注意:因为这很容易导致对当前的上下文对象的混乱和对this
,it
的混乱。
takeIf 与 takeUnless
In addition to scope functions, the standard library contains the functions
takeIf
andtakeUnless
. These functions let you embed checks of the object state in call chains.
除了作用域函数之外,Kotlin标准库还包含了takeIf
和takeUnless
两个方法。这两个方法使你可以可以在调用链中检查对象的状态。
When called on an object with a predicate provided,
takeIf
returns this object if it matches the predicate. Otherwise, it returnsnull
. So,takeIf
is a filtering function for a single object. In turn,takeUnless
returns the object if it doesn’t match the predicate andnull
if it does. The object is available as a lambda argument (it
).
当在提供了predicate的对象上调用时,takeIf
返回对象本身如果它是匹配predicate的。否则,返回null
。因此,takeIf
是一个对单个对象的过滤方法。相反的,takeUnless
返回对象本身如果它不匹配predicate,返回null
如果它匹配。这个对象在lambda中作为参数可用(it
)。
import kotlin.random.*
fun main() {
//sampleStart
val number = Random.nextInt(100)
val evenOrNull = number.takeIf { it % 2 == 0 }
val oddOrNull = number.takeUnless { it % 2 == 0 }
println("even: $evenOrNull, odd: $oddOrNull")
//sampleEnd
}
When chaining other functions after
takeIf
andtakeUnless
, don’t forget to perform the null check or the safe call (?.
) because their return value is nullable.
在takeIf
或takeUnless
后链接其他方法时不要忘记执行null检查或者使用安全调用符号?.
,因为它们的返回值是可为空的。
fun main() {
//sampleStart
val str = "Hello"
val caps = str.takeIf { it.isNotEmpty() }?.toUpperCase()
//val caps = str.takeIf { it.isNotEmpty() }.toUpperCase() //compilation error
println(caps)
//sampleEnd
}
takeIf
andtakeUnless
are especially useful together with scope functions. A good case is chaining them withlet
for running a code block on objects that match the given predicate. To do this, calltakeIf
on the object and then calllet
with a safe call (?
). For objects that don’t match the predicate,takeIf
returnsnull
andlet
isn’t invoked.
takeIf
和takeUnless
和作用域函数一起使用是非常有用的。一个合适的使用场景是使用let
链接它们,用来在一个匹配给定的 predicate 的对象上运行代码块。为了这样做,在对象上调用takeIf
然后使用?.
调用let
。对于不匹配 predicate 的对象,takeIf
返回null
并且不会被let
调用。
fun main() {
//sampleStart
fun displaySubstringPosition(input: String, sub: String) {
input.indexOf(sub).takeIf { it >= 0 }?.let {
println("The substring $sub is found in $input.")
println("Its start position is $it.")
}
}
displaySubstringPosition("010000011", "11")
displaySubstringPosition("010000011", "12")
//sampleEnd
}
This is how the same function looks without the standard library functions:
下面是不使用标准库函数时的代码:
fun main() {
//sampleStart
fun displaySubstringPosition(input: String, sub: String) {
val index = input.indexOf(sub)
if (index >= 0) {
println("The substring $sub is found in $input.")
println("Its start position is $index.")
}
}
displaySubstringPosition("010000011", "11")
displaySubstringPosition("010000011", "12")
//sampleEnd
}