Kotlin defines a few of extension functions like with()
and apply()
in its Standard.kt file. You probably have seen some of them in various tutorials or even used them already. Sometimes you may also wonder which one to use. In this post, I will walk through those confusing functions and see if we can understand the differences.
There are four (plus one added in 1.1) functions that look similar, and they are defined as follow:
|
|
If you try to read the comments, it’s very difficult to understand the differences immediately. Instead let’s read through the method signatures “literally” and look for things that are the same and ones that are different.
Things that are the same:
- T is the type of instance you want to operate with, and in Kotlin’s terminology it is called receiver type. Even though
with()
is not an extension method, the instance of T is still called receiver in method definition. - The last argument is a function that you can supply to operate the receiver object. Since it’s the last argument, so Kotlin allows you move the function declaration out of the method parenthesis.
Things that are different:
apply()
andalso()
return T, but others return R.- block in
also()
andlet()
is is a function that takes in T as the argument, but in others it is an extension function on type T.
Show me some code
Nothing is better than real code snippets, right?
Let’s start with Java version, so we can better understand how those Kotlin functions can help us write more concise and readable code at the same time as well.
|
|
See the equivalent implementations and compare the functions
also() vs apply()
Let’s compare these two functions first:
- their return value is always
this
, which is the receiver instance with type T. - the
block
returnsUnit
in both functions.
The same implementation:
|
|
the print() method is defined as for brevity:
|
|
Both functions work exactly the same way, but with one subtle difference that in also()
we have to use an explicit it
variable to append content. In apply()
we can directly call append() as if block
was part of the instance.
Why? Because block
is defined as (T) -> Unit
, but it is defined as T.() -> Unit
in apply()
.
So you can simply see apply()
as a “simpler” version of also()
that it has an implicit this defined to be used in the function body. also()
requires you to use it
, but it can be more readable in some cases, or you can even name the instance to fit your context better.
let() vs run()
Comparison:
block
is defined as(T) -> R
inlet()
, but it is defined asT.() -> R
inrun()
- their return value is
R
fromblock
function.
Implementation:
|
|
Similar to the previous comparison, let()
requires an explicit it and run()
has an implicit this in their block
body.
However, this pair of functions has another major difference than also()
and apply()
. They return the value returned by the block
body. In other words, both let()
and run()
return whatever block
returns.
See the following sample:
|
|
The first one returns the enclosing string builder
since append()
returns the string builder itself, but the second one returns 37
since length()
returns an integer.
So both let()
and run()
can be used when you want to operate an instance of T but you also want to have a different return value R.
with()
Implementation:
|
|
with()
is not an extension function on type T, but it takes a instance of T as the first argument. Also Kotlin allows us to move the last function argument out of the parenthesis.
block
is defined as T.() -> R
so you don’t have to use it
, and you can change the return value in block
body.
Comparison Chart
I made a simple comparison chart to include the characteristics and hopefully we can see the differences more easily:
Name | Is Extension Function? | Return Type | Argument in block | block definition |
---|---|---|---|---|
also() | yes | T (this) | explicit it | (T) -> Unit |
apply() | yes | T (this) | implicit this | T.() -> Unit |
let() | yes | R (from block body) | explicit it | (T) -> R |
run() | yes | R (from block body) | implicit this | T.() -> R |
with() | no | R (from block body) | implicit this | T.() -> R |
Thoughts:
These functions looks very similar and can be interchangable in some use cases. For better consistency and readability for Kotlin beginners, I would suggest that maybe start with also()
and let()
since they require developers to name the argument or use the explicit it
in the block body. It may be slightly verbose than other functions, but it is always good to have some common ground among your team members when starting using Kotlin.