How it works
The explanation, with examples you can run.
Two questions
| Function | Object inside the block | Returns |
|---|---|---|
let | it | the block's result |
also | it | the object |
run | this | the block's result |
apply | this | the object |
with(obj) | this | the block's result (not an extension) |
this is a lambda with a receiver (T.() -> R): you can call the object's members without a prefix. it is a plain parameter ((T) -> R), which you can rename (let { user -> ... }).
One typical use each
data class Request(var url: String = "", var timeoutMs: Int = 0, val headers: MutableMap<String, String> = mutableMapOf()) fun main() { // apply: configure an object, get the object back val req = Request().apply { url = "https://example.com" timeoutMs = 5_000 headers["Accept"] = "application/json" } // also: a side effect in the middle of a chain, without changing the value val ids = listOf(3, 1, 2).sorted().also { println("sorted: $it") } // let: work with a nullable value, or transform one value into another val header: String? = req.headers["Accept"] val length = header?.let { it.length } ?: 0 // run / with: compute a result from an object's members val summary = with(req) { "$url (${timeoutMs}ms, ${headers.size} headers)" } println(ids) // [1, 2, 3] println(length) // 16 println(summary) // https://example.com (5000ms, 1 headers)}When they hurt
- Nesting. Inside
a.apply { b.let { ... } },thisisaanditisb; one more level and nobody can tell whichthisis meant. Name the parameter, or use a localval. ?.let { } ?: ...as if-else. The Elvis also fires when the block itself returnsnull, which is easy to miss. For two real branches, writeif (x != null) ... else ...(smart casts make it just as short).- Shadowing. In
apply, a property of the object hides a local variable or a property of the enclosing class with the same name.name = nameinsideapplyassigns the object's property to itself. - Long blocks. A 30-line
run { }is a function without a name. Give it one. letjust to call one function.x.let { foo(it) }isfoo(x), unlessxis nullable.
takeIf { } and takeUnless { } are the close relatives: input.takeIf { it.isNotBlank() } returns the object or null, and pairs well with ?.let.
The Kotlin documentation gives the same advice: choose by purpose, avoid nesting, take care when chaining, and if the block grows, prefer plain code. Like most style rules, the goal is that the next reader sees the intent without counting receivers.
Check yourself
3 questions. Get them all right to finish this topic.
0 of 3 right so far. Get all three to finish this topic.