Scope Functions in Kotlin (let, apply, with, also, run)
Sometimes while writing code we want to execute a few lines of code referencing an object. For that, we create a temporary scope in which we can access that object without the object name. These functions do not give any technical capabilities but enhance readability and conciseness.
All these functions do the same execute a block of code on an object
.
Let's see how they differ from each other.
Let
Used to add a null check on objects.
val name: String? = "Kotlin"
val result = name?.let {
println("The name is $it") // 'it' refers to 'name'
it.length // Returns the length of the string
}
println(result) // Output: 6
If let's say name is null. Then
val name: String? = null
val result = name?.let {
println("The name is $it") // This won't be printed
it.length
}
println(result) // Output: null
With
In simple words, you can use with
it is when you want to perform several operations on an object and potentially return a result.In the code, with
can be read as “With this object, do the following.”
Let's understand this by example: —
class Person {
var name: String = ""
var age: Int = 0
var job: String = ""
var city: String = ""
}
fun main() {
val person = Person()
// Setting properties without 'with'
person.name = "Alice"
person.age = 30
person.job = "Engineer"
person.city = "New York"
// Printing the details
println("Name: ${person.name}, Age: ${person.age}, Job: ${person.job}, City: ${person.city}")
}
Here you can see we are repeatedly referencing the object (person
in this case) for each operation. Is there any better way to write this code?
Let’s see -
fun main() {
val person = Person()
// Using 'with' to set properties
with(person) {
name = "Alice"
age = 30
job = "Engineer"
city = "New York"
}
// Printing the details
println("Name: ${person.name}, Age: ${person.age}, Job: ${person.job}, City: ${person.city}")
}
You can see now it's more concise and readable.
Lets see one more example when you are using with
and returning value from with block
val person = Person()
// Using 'with' to set properties and return a summary string
val summary = with(person) {
name = "Alice"
age = 30
job = "Engineer"
city = "New York"
// Return a summary string
"Name: $name, Age: $age, Job: $job, City: $city"
}
// Printing the summary
println(summary)
Simply you can write whatever you want to return as a last statement in with
block.
APPLY
Use apply
for code blocks that don't return a value and mainly operate on the members of the receiver object. The common case apply
is the object configuration. Such calls can be read as “apply the following assignments to the object.”
Let's see how can we use apply
in the above example:-
fun main() {
// Using 'apply' to configure the object
val person = Person().apply {
name = "Alice"
age = 30
job = "Engineer"
city = "New York"
}
// Printing the details
println("Name: ${person.name}, Age: ${person.age}, Job: ${person.job}, City: ${person.city}")
}
It executes a block of code on an object and then returns the object itself. It is useful when you want to modify an object and then continue to use that object.
We can see there are 2 main differences in with
and apply
.
Return Value:
apply
returns the receiver object itself.
with
returns the result of the block.Use Case:
Use
apply
when you need to configure or initialize an object and continue using it.Use
with
when you want to execute a block of code on an object and return a result from that block.
ALSO
also
is used to perform additional actions on an object while returning the object itself. It’s often used for logging, validation, or other side effects without changing the object.
Let’s see by example:-
fun main() {
val person = Person().also {
// Perform actions like logging or validation
println("Setting properties for a new person.")
it.name = "Alice"
it.age = 30
it.job = "Engineer"
it.city = "New York"
}
// Printing the details
println("Name: ${person.name}, Age: ${person.age}, Job: ${person.job}, City: ${person.city}")
}
Run
run
is used to execute a block of code on an object or a lambda expression and return the result of the block. It can be used both as a method on an object or as a standalone function.
Run using a standalone function
fun main() {
val result = run {
val a = 10
val b = 20
a + b // The result of the block is the sum of a and b
}
// Printing the result
println(result) // Output: 30
}
Using run
with an Object:
fun main() {
val person = Person().run {
// Configure the object
name = "Alice"
age = 30
job = "Engineer"
city = "New York"
// Return a summary string
"Name: $name, Age: $age, Job: $job, City: $city"
}
// Printing the result
println(person) // Output: Name: Alice, Age: 30, Job: Engineer, City: New York
}
Let’s see a summary of what we have learned so far:-
Summary
let
: Use for null-safe operations and transformations. Returns the result of the block.apply
: Use for configuring or initializing an object. Returns the receiver object itself.run
: Use for executing a block of code and obtaining a result. Can be used with or without an object.also
: Use for performing additional actions (e.g., logging) while keeping the original object unchanged. Returns the receiver object itself.with
: Use for performing multiple operations on an object and returning a result. Executes on a non-null object.