There are three dimensions you can differentiate between the five scoping functions
this
or it
inside the lambda, respectivelywith
is the only one that accepts an explicit parameter)Using let
The let
function is a useful tool for variable scoping and to work with nullable types. It has three main use cases.
null
First, variable scoping can be used whenever a variable or object should only be used in a small region of the code, for instance, a buffered reader reading from a file, as shown in following listing.
val lines = File("rawdata.csv").bufferedReader().let { val result = it.readLines() it.close() result }
This way, the buffered reader object and all variables declared inside the let
block (the lambda expression) are only visible inside that block. This avoids unwanted access to the reader from outside this block. Notice that, similar to if
and when
expressions, let
returns the value defined in the last line of the lambda. This example returns a list of all lines in a file and must store that into a temporary variable in order to call close in between and close the file - we will improve this later with use
.
Second, let
is useful to work with nullable types because, when called on a nullable object using the safe call operator, the let
block will only be executed if that object is not null. Otherwise, the let
block will simply be ignored. Imagine trying to fetch weather data with a network call that may return null
, as illustrated in following listing.
val weather: Weather? = fetchWeatherOrNull() weather?.let { updateUi(weather.temperature) }
When using let
with a safecall operator like this, then inside the let
block, the Kotlin compiler helps you by automatically casting the weather
variable to a non-nullable type so that it can be used without further addressing nullability. If the weather data could not be fetched, the lambda passed to let
is not run. Thus, the UI will not be updated.
Lastly, you can convert one nullable object into another with the same construct as in above listing by calling let
on the original nullable object and defining the desired converted value in the last line inside the let
block. This last line defines the return value of let
.
Using apply
The higher-order function apply
has two primary use cases.
Following listing shows how to encapsulate multiple calls on the same object into an own code block. In the following subsections, let’s assume countryToCapital
was a mutable map, not a readonly one, so that you can add new elements to it.
countryToCapital.apply { putIfAbsent("India", "Delhi") putIfAbsent("France", "Paris") }
With apply
, you do not refer to it because the lambda you pass in is executed like an extension function on the object that you call apply
on, here countryToCapital
. Thus, you can write the code inside the apply
block as if it were part of the MutableMap
class. You could write this.putIfAbsent
, but using this
as a prefix is optional.
Most commonly, apply
is used for object initialization as in following listing.
val container = Container().apply { size = Dimension(1024, 800) font = Font.decode("Arial-bold-22") isVisible = true }
This approach uses the fact that apply
first runs any code in its lambda and then returns the object that it was called on. Thus, the container variable includes any changes made to the object inside the lambda. This also enables chaining apply
with other calls on the object, as shown in following listing.
countryToCapital.apply { ... } .filter { ... } .map { ... }
Using with
The with
function behaves almost like apply
and has two major use cases.
In contrast to apply
, it returns the result of the lambda, not the object on which it was called. This is shown in following listing.
val countryToCapital = mutableMapOf("Germany" to "Berlin") val countries = with(countryToCapital) { putIfAbsent("England", "London") putIfAbsent("Spain", "Madrid") keys }
This shows that the return value of with
is the return value of the lambda you pass in - defined by the last line inside the lambda. Here, the expression in the last line is the keys of the map, so the with block returns all countries stored as keys in the map.
Similar to let
, you can limit the scope of an object to only the lambda expression if you create that object adhoc when passing it into with. This is demonstrated in following listing.
val essay = with(StringBuilder()) { appendln("Intro") appendln("Content") appendln("Conclusion") toString() }
Note that with
is superior to apply
in this case because you don’t want to get the string builder back as a result but rather the lambda’s result. Builders like this are the most common example, where with
is preferable to apply
.
Using run
The run
function is helpful to:
let
but then use this
inside the lambda instead of it
.First, run
can be seen as a combination of with
and let
. Imagine that countryToCapital
was nullable, and that you want to call multiple methods on it, as shown in following listing.
val countryToCapital: MutableMap<String, String>? = mutableMapOf("Germany" to "Berlin") val countries = countryToCapital?.run { putIfAbsent("Mexico", "Mexico City") putIfAbsent("Germany", "Berlin") keys }
It fulfills the same purpose as with
listing but skips all operations in the lambda if the map is null.
Next, you can use run
for immediate function application, which means plainly running a given lambda. This is shown in following listing.
val success = run { // Only visible inside this lambda val username = getUsername() val password = getPassword() validate(username, password) }
Here, username
and password
are only accessible where they really have to be. Reducing scopes like this is a good practice to avoid unwanted accesses.
Lastly, you can use run
to transform an explicit parameter into a receiver object if you prefer working with it that way. This is demonstrated in following listing, which assumes a User
class that has a username
.
fun renderUsername(user: User) = user.run { val premium = if (paid) " (Premium)" else "" val displayName = "$username$premium" println(displayName) }
Inside the lambda expression, you can access all members of the User
class directly (or via this
). This is especially useful if the parameter name is long and you have to refer to it often inside the function.
Using also
This last higher-order function from Standard.kt
has two main use cases:
First, consider following listing for an example that performs validation using also
.
val user = fetchUser().also { requireNotNull(it) require(it!!.monthlyFee > 0) }
First, the fetchUser
function is called and may return null
. After that, the lambda is executed, and only if it runs through is the variable assigned to the user. The usefulness of this function for logging and other operations "on the side" only becomes apparent in a chain of function calls, as in following listing.
users.filter { it.age > 21 } .also { println("${it.size} adult users found.") } .map { it.monthlyFee }
Outside of such chains, there’s no direct need to use also
because you could do logging and other side operations on the next line. In call chains, however, also lets you intercept the intermediate results without breaking the chain.
Using use
The use
function is not part of Standard.kt
but has a similar structure and benefits. It ensures that the resource it is called on is closed after the given operations are performed. For this reason, it is only defined for subtypes of Closeable
like Reader
, Writer
, or Socket
. Using this, you can improve the code from first listing as shown below
val lines = File("rawdata.csv").bufferedReader().use { it.readLines() }
Since the buffered reader is now closed automatically at the end of the use
block, there is no need to store the result in a temporary variable, and you can reduce the lambda to a single line. Apart from ensuring close is called, use
is just like let
. Internally, it uses a finally
block to ensure the Closeable
instance is ultimately closed.