The Kotlin library contains several functions whose sole purpose is to execute a block of code in the context of an object. When such a function is called on an object and a lambda expression is provided, it forms a temporary scope. In this scope, the object can be accessed without its name. These functions are called
Scope function
. There are five types: let, run, with, apply and also.
These functions basically do the same thing: execute a block of code on an object. The difference is how this object is used in the block and what the result of the entire expression is.
Here is a typical use of a scope function:
Person("Alice", 20, "Amsterdam").let {println(it) it.moveto ("London") it.incrementage () println(it)} You must introduce a new variable and repeat its name each time you use it. val alice = Person("Alice", 20, "Amsterdam") println(alice) alice.moveTo("London") alice.incrementAge() println(alice)Copy the code
Scoped functions don’t introduce any new techniques, but they can make your code cleaner and more readable.
Choosing the right function for your case can be a bit tricky due to the similar nature of scoped functions. The choice depends largely on your intentions and consistency of use in the project. Below we describe in detail the differences between the various scoped functions and their convention usage.
The difference between
Because scoped functions are all very similar in nature, it is important to understand the differences between them. There are two main differences between each scope function:
- The way context objects are referenced
- The return value
Context object: This or it
In lambda expressions of scoped functions, context objects can be accessed using a shorter reference rather than their actual name. Each scoped function accesses the context object in one of two ways: as the receiver of a lambda expression (this) or as the argument to a lambda expression (it). Both offer the same functionality, so we’ll describe the pros and cons of both for different scenarios and provide recommendations for their use.
fun main() { val str = "Hello" // this str.run { println("The receiver string length: $length") //println("The receiver string length: Let {println("The receiver string's length is ${it. Length}")}}Copy the code
this
Run, with, and apply refer to context objects through the keyword this. Therefore, context objects can be accessed in their lambda expressions just as they would in normal class functions. In most scenarios, you can omit this when accessing the receiver object to make your code shorter. By contrast, if this is omitted, it becomes difficult to distinguish between members of the recipient object and external objects or functions. Therefore, for lambda expressions that operate primarily on object members (calling their functions or assigning their attributes), it is recommended that the context object be the receiver (this).
Val Adam = Person("Adam"). Apply {age = 20 and this. Age = 20 or adam.age = 20 city = "London"} println(Adam)Copy the code
it
In turn, let and also take context objects as lambda expression arguments. If no parameter name is specified, the object can be accessed with the implicit default name it. It is shorter than this, and expressions with it are usually easier to read. However, when calling object functions or properties, you cannot access objects implicitly like this. Therefore, it is better to use it as the context object when the context object is primarily used as a parameter in a function call in scope. It is also better if you use multiple variables in a code block.
fun getRandomInt(): Int {
return Random.nextInt(100).also {
writeToLog("getRandomInt() generated value $it")
}
}
val i = getRandomInt()
Copy the code
In addition, when a context object is passed as a parameter, a custom name in scope can be specified for the context object.
fun getRandomInt(): Int {
return Random.nextInt(100).also { value ->
writeToLog("getRandomInt() generated value $value")
}
}
val i = getRandomInt()
Copy the code
The return value
Based on the result returned, scope functions can be classified into the following two categories:
apply
及also
Returns a context object.let
,run
及with
Returns the result of the lambda expression.
These two options allow you to select the appropriate function based on subsequent actions in the code.
Context object
The return value of apply and also is the context object itself. Therefore, they can be included in the call chain as auxiliary steps: you can continue to make chained function calls on the same object.
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()Copy the code
They can also be used in return statements of functions that return context objects.
fun getRandomInt(): Int {
return Random.nextInt(100).also {
writeToLog("getRandomInt() generated value $it")
}
}
val i = getRandomInt()
Copy the code
Lambda expression result
Let, run, and with return the result of a lambda expression. So you can use them when you need to assign a value to a variable using their results, or when you need to chain the results, etc.
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.")
Copy the code
Alternatively, you can create a temporary scope for a variable using only the scope function, ignoring the return value.
val numbers = mutableListOf("one", "two", "three")
with(numbers) {
val firstItem = first()
val lastItem = last()
println("First item: $firstItem, last item: $lastItem")
}
Copy the code
Several functions
To help you choose the right scope functions for your scenario, we describe them in detail and provide some suggestions for their use. Technically, scoped functions are interchangeable in many scenarios, so these examples show the use of conventions that define a common usage style.
let
The context object is accessed as an argument (it) to a lambda expression. The return value is the result of a lambda expression.
Let can be used to call one or more functions on the result of the call chain. For example, the following code prints the results of two operations on a collection:
val numbers = mutableListOf("one", "two", "three", "four", "five")
val resultList = numbers.map { it.length }.filter { it > 3 }
println(resultList)
Copy the code
Using let, we can write it like this:
val numbers = mutableListOf("one", "two", "three", "four", "Five ") numbers. Map {it. Length}.filter {it > 3}.let {println(it) // Call more functions if needed}Copy the code
If the code block contains only a single function that takes it as an argument, a method reference (::) can be used instead of a lambda expression:
val numbers = mutableListOf("one", "two", "three", "four", "five")
numbers.map { it.length }.filter { it > 3 }.let(::println)
Copy the code
Let is often used to execute a block of code with only non-null values. If you need to operate on a non-empty object, you can use the safe call operator on it. . And calls let to perform operations in lambda expressions.
val str: String? // compiler error: STR may be null val length = STR? .let {println("let() called on $it") processNonNullString(it) .let {}' must not be empty it.length}Copy the code
Another way to use lets is to introduce scoped local variables to improve code readability. If you need to define a new variable for a context object, you can provide its name as a lambda expression parameter instead of the default 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'")Copy the code
with
A non-extension function: The context object is passed as an argument, but inside a lambda expression, it can be used as a receiver (this). The return value is the result of a lambda expression.
We recommend using with to call functions on context objects rather than lambda expression results. In code, with can be read as”
For this object, do the following.
“
val numbers = mutableListOf("one", "two", "three")
with(numbers) {
println("'with' is called with argument $this")
println("It contains $size elements")
}
Copy the code
Another use scenario for with is to introduce a helper object whose properties or functions will be used to evaluate a value.
val numbers = mutableListOf("one", "two", "three")
val firstAndLast = with(numbers) {
"The first element is ${first()}," +
" the last element is ${last()}"
}
println(firstAndLast)
Copy the code
run
The context object is accessed as the receiver (this). The return value is the result of a lambda expression.
Run and with do the same thing, but are called in the same way as let — as extension functions for context objects.
Run is useful when lambda expressions include both object initialization and evaluation of return values.
val service = MultiportService("https://example.kotlinlang.org", 80) val result = service.run {port = 8080 query(prepareRequest() + "to port $port")} Val letResult = service.let {it.port = 8080 it.query(it.Preparerequest () + "to port ${it.port}")} In addition to calling run on the receiver object, It can also be used as a non-extension function. A non-extended run lets you execute a block of statements where an expression is needed. 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) }Copy the code
apply
The context object is accessed as the receiver (this). The return value is the context object itself.
Use Apply for blocks of code that return no value and run primarily on members of the receiver (this) object. A common case for Apply is object configuration. Such a call can be interpreted as applying the following assignment operations to the object
Val Adam = Person("Adam").apply {age = 32 city = "London"} println(Adam) returns the receiver and you can easily include apply in the call chain for more complex processing.Copy the code
also
The context object is accessed as an argument (it) to a lambda expression. The return value is the context object itself.
Also is useful for performing operations that take context objects as arguments. Use also for operations that need to refer to an object rather than its properties and functions, or when you don’t want to mask this references from an external scope.
When you see also in code, you can interpret it as and do the following with that object
val numbers = mutableListOf("one", "two", "three")
numbers
.also { println("The list elements before adding new one: $it") }
.add("four")
Copy the code
Function to choose
To help you choose the right scope function, we have provided a table of the main differences between them.
Here is a short guide to selecting scoped functions for the intended purpose:
- To introduce an expression as a variable into a local scope:
let
- Object configuration:
apply
- Object configuration and calculation results:
run
- Run statements where expressions are needed: non-extended
run
- Additional effects:
also
- A set of function calls to an object:
with
There are overlapping scenarios for the use of different functions, and you can choose the functions based on the specific conventions used on your project or team.
While scoped functions are a way to make your code more concise, avoid overusing them: they can make your code less readable and can lead to errors. Avoid nesting scoped functions, and be careful when calling them chained: it’s easy to get confused about the current context object and the value of this or it.
TakeIf and takeUnless
In addition to the scoped functions, the library also contains the takeIf and takeUnless functions. These functions allow you to embed object state checks in the call chain.
When called on an object with the provided predicate, takeIf returns the object if it matches the predicate. Otherwise null is returned. Therefore, takeIf is a filter function for a single object. TakeUnless, on the other hand, returns an object if it does not match the predicate and null if it does. This object is accessed as a lambda expression parameter (it).
val number = Random.nextInt(100) val evenOrNull = number.takeIf { it % 2 == 0 } val oddOrNull = number.takeUnless { it % 2 == 0 } println("even: $evenOrNull, odd: $oddOrNull")Copy the code
When chain-calling other functions after takeIf and takeUnless, don’t forget to perform null checks or security calls (? .). Because their return value is nullable.
val str = "Hello" val caps = str.takeIf { it.isNotEmpty() }? .toupperCase () //val caps = str.takeIf {it.isnotempty ()}.toupperCase () // Error println(caps)Copy the code
TakeIf and takeUnless are particularly useful together with scoped functions. A good example is to link them with a let to run a code block on an object that matches a given predicate. To do this, call takeIf on the object and then use a security call (? .). Calls to let. For objects that do not match the predicate, takeIf returns NULL and does not call let.
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")Copy the code
Without the library function, the same function looks like this:
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")
Copy the code