Scoping functions
On the one hand, scoping functions offer the possibility of meaningfully and expressively restricting the scope of variables and, on the other hand, of bundling associated code in a clear and legible manner, along with a compact notation.
Often times one writes code, which first creates a new object and shortly thereafter invokes some of its methods or sets some properties.
In Java, this might look like this:
Person person = new Person();
person.setFirstName("Alex");
person.setLastName("Example");
person.setDateOfBirth(1984,3,5);
person.calculateAge();
Thanks to Kotlin’s properties, you can write that more elegantly:
val person = Person()
person.firstName = "Alex"
person.lastName = "Example"
person.setDateOfBirth(1984,3,5)
person.calculateAge()
It gets even better with the first possible scoping function called apply
:
val person = Person().apply {
firstName = "Alex"
lastName = "Example"
setDateOfBirth (1984,3,5)
calculateAge ()
}
And this is what the signature of apply
looks like:
inline fun <T> T.apply(block: T.() -> Unit) : T
The function calls the passed block
with this
as its receiver and returns this
.
So within the block
you can access the calling object (the receiver) with this
.
And as usual, you can omit this
, which makes the code so readable.
What makes apply
even more versatile is the fact that it implicitly returns this
.
This allows, as seen above, to create an object and fully prepare it in one call.
With apply
, the creation, initialization and assignment of an object can be elegantly and compactly encapsulated in a single statement - resulting in very compact, structured and easy-to-read code.
let
, run
and also
In addition to apply
, there are a few other similar functions that differ in how you access the calling object within the block and whether you return this
(as with apply
) or the result of the block.
it
or this
?
As described above, you can access the original object in apply
by this
(and can omit this
, as usual, for better readability).
Besides apply
there is an almost identical function called also
, which differs only in that one does not access the original object by this
but it
:
val person = Person().also {
it.firstName = "Alex"
it.lastName = "Example"
it.setDateOfBirth (1984,3,5)
it.calculateAge ()
}
Incidentally, unlike this
, it
can’t be left out, which lowers the readability.
That’s why apply
usually is the preferred choice, unless your goal is to avoid shadowing.
The return value
We got to know apply
and also
, which differ only in whether one accesses the calling object via this
or it
.
Both return the receiver (this
for apply
, it
for also
).
But sometimes you just do not want to return the receiver, but something else.
There are the counterparts run
and let
for that.
They do not return the calling object, but rather what the called block returns.
Just like apply
and also
, the only difference between run
and let
is whether the receiver is referenced by this
or it
.
The following table summarizes all this:
Function | Reference to caller | Return value |
---|---|---|
apply | this | caller (this ) |
also | it | caller (it ) |
run | this | block’s return value |
let | it | block’s return value |
Similar functions
In addition to the generic scoping functions mentioned above, there are a number of similar functions in Kotlin that are less generic but have a specific purpose.
The function use
use
is the counterpart to Java’s try-with-resources
, which allows you to encapsulate AutoClosables
, closing them “automatically” in a more readable way than ordinary try-catch
blocks.
In the following example
- we refer to a file
- open a
Writer
on it - encapsulate it with
use
- use the
Writer
(it
) to write a text to the file - close the
Writer
properly
File("test_file").writer().use {
it.write("Hello World!")
}
What stands out is that the last step - closing the Writer
- does not even appear in the code explicitly.
Because that’s exactly what use
does for us ‘behind the scenes’.