Simple Way to Understand Kotlin Scope Functions - let, run, with, apply, also
You have no idea when to use them because they are confusing. This article explains the common practical usages of these Scope functions.
If you look at the scope function examples in my previous post here, you can clearly tell that all those Kotlin Scope Functions (i.e. let
, run
, with
, apply
, also
) can accomplish the same thing. They're all interchangeable, with slightly different in syntax.
Syntax Differences
Let's understand the syntax differences between them first.
Source: kotlinlang.org
Note 1: All these scope functions are extension functions except for
run
andwith
. There are 2run
scope functions. One is with extension and another one without the extension function.
val str = "test"
// let, run, also, apply - are "extension functions"
str.run {
println(this)
}
// with - is NOT an "extension function"
with(str) {
println(this)
}
// run - could be NOT an "extension function"
run {
println(str)
}
Note 2: Return value is lambda result means return the last line returned value in the lambda expression. All scope functions return value is lambda result except for
apply
andalso
which context object is returned.
val str = "test"
/* let, run, with - return last returned value in the lambda expression */
val tmp1 = str.run {
// return Kotlin.Unit (equivalent to void in Java)
println(this)
}
// this will print out "Kotlin.Unit"
println(tmp1)
// apply, also - always return the object context (i.e. str)
val tmp2 = str.apply {
println(this)
}
// tmp2 is str, this will print out "test"
println(tmp2)
Note 3: Within the scope, we can reference the object either through
this
orit
.
run
,with
andapply
usethis
.let
andalso
useit
val str = "test"
/* run, with, apply - object reference is "this" */
str.run {
println(this)
}
// let, also - object reference is "it"
str.let {
println(it)
}
It is confusing...
I must admit, trying to understand all these differences make me even more confused! This is because the syntax itself doesn't explain when and how exactly we should use them?
The crucial part is not the syntax but their usages but how and when developer usually use them? Let's look at more practical usages of them in the following!
Let's assume we have the following class.
class Dog(val name: String) {
var age: Int = 0
var owner: String? = null
fun bark() = println("$name is barking")
fun eat() = println("$name is eating")
}
When to Use let
?
let
is typically used for nullable object to avoid NullPointerException
Replace null check
var nullableDog: Dog? = null
if (nullableDog != null) {
nullableDog.age++
nullableDog.owner = "Vincent"
nullableDog.bark()
nullableDog.eat()
}
val age = nullableDog?.age
with let
var nullableDog: Dog? = null
val age = nullableDog?.let {
it.owner = "Vincent"
it.bark()
it.eat()
it.age++
}
it
can be replaced by any name for readability. This is especially useful if you have chained / nested scope functions, where the name can be used to differentiate between inner vs outer scope.
val age = nullableDog?.let { myDog ->
myDog.owner = "Vincent"
myDog.bark()
myDog.eat()
myDog.age++
}
Note: This example above is NOT a nested scope function. I personally hate it and will avoid it
If you don't need this readability, let
is pretty useless in my opinion because it can be completely replaced by run
and you don't need to specify the it
to reference the context object.
When to Use run
?
run
usage is similar to let
which makes your code readable to perform the null check. Since this
can be omitted in the lambda expression, I feel that the code is cleaner. Thus, I personally would prefer run
over let
.
var nullableDog: Dog? = null
val age = nullableDog?.run {
owner = "Vincent"
bark()
eat()
age++
}
When to Use run
Non-extension Function?
run
with non-extension function is very similar to standard scope function in other languages. dog
variable in the inner scope refers to Jimmy
and dog
variable in the outer scope refers to Lily
val dog = Dog("Lily")
run {
val dog = Dog("Jimmy")
println("Dog name is ${dog.name}")
}
println("Dog age is ${dog.name}")
Note: This is probably rarely used. I haven't seen any code that uses this. Separating out to a different function is probably a better idea than using the
run
non-extension function, scope function.
When to Use with
?
with
is similar to run
except it is used when you want to operate on a non-null object. If your object is nullable, do NOT use with
.
val dog = Dog("Lily")
val age = with(dog) {
owner = "Vincent"
bark()
eat()
age++
}
When to Use apply
?
apply
is usually used to initialize the properties in the class during the object creation.
You replace this
val dog = Dog("Lily")
dog.owner = "Vincent"
dog.age = 10
with
val dog = Dog("Lily").apply {
owner = "Vincent"
age = 10
}
When to Use also
?
also
is pretty much the same as apply
and we rarely need it. Similar to let
, it is probably needed for readability issue in nested scope functions, where you can replace it
with a more meaningful name.
val dog = Dog("Lily").also {
it.owner = "Vincent"
it.age = 10
}
Summary
This is how I'm going to use these scope functions:
- let - do NOT use this for null check unless for readability issue in nested / chained scope functions
- run - use this for null check instead. However, if you need to use
this
for object reference, consider to uselet
instead for readability - run (non-extension function) - probably NOT required, replace this scope with a function call instead for better readability
- with - use this for non-nullable object
- apply - use this for object creation and initialization
- also - do NOT use this, unless for readability issue in nested scope function. Use
apply
instead.
The purpose of scope function is to reduce code and make your code readable. So, it is really up to you how you would like to use them to achieve it. Sometimes this could be just a personal thing.
For example, what I suggested here (i.e. do NOT use let
for null check) probably goes against the majority, but I find run
is more straight forward. It makes my options simple. I just need to choose between run
and with
- use
run
for nullable variable - use
with
for non-nullable variable.
Isn't this simple?
Scope function is a nice-to-have feature. Even if you don't use them, it is fine too as long as your code is still readable.