We have already figured out how scope functions apply, also, with, let, and run work. However, we have also encountered a few complicated issues: some of the functions can be used interchangeably, and there is also some confusion with choosing it or this when accessing the context object. Now it's time to cut this famous "Kotlinian knot"! We will learn how to choose the right function and why we need these two similar keywords: it and this. Let's get started!
What is the difference: result of functions
There is an important difference between the scope functions, which greatly influences our choice of the most appropriate one.
1. apply and also return context objects.
2. let, run, and with return a lambda result.
With apply and also, we change the original context object and can make a long chain of functions around it. It's called side steps.
fun main() {
val numbers = mutableListOf<Int>()
numbers.also { println("Let's add some elements in this list") }
.apply {
add(2)
add(3)
add(1)
}
.also { println("And now let's sort these elements in the list") }
.sort() // also() and sort() get as a parameter our changed context object - numbers
println(numbers)
}
We performed all these operations on the same object, our mutableList(). An alternative way is using apply and also with return. In this case, we can decide what we want to return.
Unlike the case in the first example, let, run, and with return some result, and as a result Kotlin takes the last expression in the function's body. Therefore, we can assign the result of these functions to a variable or make a long chain and transform different data.
fun main() {
val numbers = mutableListOf<Int>(2, 6, 8, 9, 74)
val bigNumbers = numbers.run {
add(15) // Remember that run uses this
add(5)
count { it > 10 }
} // If you put here another method or function, it will receive as a parameter the result of the last expression, the number of elements greater than ten
println("There are $bigNumbers numbers greater than ten")
}
Another way to use these functions is making a temporary scope for a variable, performing some actions, getting the results, and forgetting about it like a little guppy fish forgets that it already had lunch.
fun main() {
val numbers = mutableListOf<Int>(2, 6, 8, 9, 74)
with(numbers) {
val firstNum = first() // Remember that with uses this
val lastNum = last()
println("First element is $firstNum, last element is $lastNum")
}
}What is the difference: context object
In terms of scope functions, there are five practical differences between this and it.
thisrefers directly to the context object, whileitcan refer to separate members of the object on which we invoke a scope function.In both cases, we refer to some object with which we will work, but with
itwe can pass several parameters, while withthiswe're dealing with one function parameter.We can omit
thisin a function and access the arguments or object methods directly, but in the case ofit, we have to call arguments and parameters with theitkeyword.With
itwe can rename our parameters and use a more appropriate name inside the function.If you have only one function in the body and
itas a parameter, you can replace that with the::reference to the function.
run, with, and apply work with this, while let and also work with it. Let's see some simple examples.
data class Musician(var name: String, var instrument: String = "Guitar", var band: String = "Radiohead")
fun main() {
Musician("Jonny Greenwood").apply {
instrument = "harmonica" // here we can use this.instrument
band = "Pavement"
}
Musician("Jonny Greenwood").also {
it.instrument = "harmonica"
}
Musician("Dave Glowl", "Drums", "Nirvana").let { (musicianName, instr, newBand) ->
musicianName.length + instr.length + newBand.count { it == 'a' }
}
// With it, we can pass several parameters and use them as separate parameter members. We can also rename these parameters
}
Don't get scared: the use of it and this is almost the same, we only show their technical differences. In practice, they can play similar roles and you can choose the most suitable way.
Now, look at the table, which summarizes the differences between scope functions.
Function | Object reference | Return value | Is extension function |
|---|---|---|---|
|
| Lambda result | Yes |
|
| Lambda result | Yes |
| - | Lambda result | No: called without the context object |
|
| Lambda result | No: takes the context object as an argument |
|
| Context object | Yes |
|
| Context object | Yes |
Function selection
Once again, like in the previous topic, you might think: "Hey, I can receive the same result if I, for example, replace run with let and add the word it to a parameter inside the function body. And I can replace run with with." Indeed, in many cases you can do so.
And here is a cool thing: you can define your own rules for scope function usage in your team.
Why is it cool? Because you will know: ok, if I see run, I understand that we use it for certain purposes. You see it and you immediately understand: yes, this piece of code does the job! And that is the beauty and purpose of scope functions: they can help you make code more readable and clear. We can say that scope functions are primarily semantic elements.
However, Kotlin language creators give us the following recommendations (they refer to them in Kotlin coding conventions):
Executing a lambda on non-null objects:
letIntroducing an expression as a variable in local scope:
letObject configuration:
applyObject configuration and computing the result:
runRunning statements where an expression is required: non-extension
runAdditional effects:
alsoGrouping function calls on an object:
with
In our two previous topics about scope functions, we already used these official recommendations in our examples.
Conclusion
We have considered an important and complicated topic – how to use scope functions. Why is it so important? Because you will certainly encounter these functions in real practice no matter what you work on: Android apps, Kotlin backend, or something else. So, here's what we've learned:
You can define use cases for different scope functions in your team.
You can skip the keyword
thisbut must useitas a reference to your context object in scope functions.Knowing that different scope functions return different results, you can make function chains. However, be careful with chains: scope functions must make code more readable, while long chains make it more confusing.