Explain what happens when you create lambdas inside a loop that capture the loop variable or a var updated in the loop. How does Kotlin's behavior compare to the classic capture pitfall, and how do you get a per-iteration snapshot?
answer
- Capture by variable, not by value
- Shared var -> all closures see final value
- Fix: val snapshot per iteration
- for-loop var is re-bound each iteration
- Bites with deferred/coroutine/callback execution
basics
~10 sIf lambdas capture one shared mutable var, they all see its final value. To freeze the current value per iteration, copy it into a new val inside the loop and capture that instead.
solid answer
~40 sCapture is by reference to the variable, not by value. If many lambdas capture the same mutable var, they all share one Ref and observe whatever value it holds when they finally run — typically the last value after the loop ends. The classic fix is to introduce a fresh val per iteration and capture it, so each closure freezes its own snapshot. Kotlin's for-loop iteration variable (for (x in list)) is effectively a new binding per iteration, so capturing it directly usually gives the expected per-iteration value — unlike a single var counter you mutate yourself, which is shared. The reliable, intention-revealing pattern is `val snapshot = current` inside the loop body. This matters for deferred execution: event handlers, coroutines, callbacks registered in a loop.
code
kotlin · 12 linesimport kotlinx.coroutines.*
fun main() = runBlocking {
var i = 0
val jobs = mutableListOf<Job>()
while (i < 3) {
val snapshot = i // freeze per-iteration value
jobs += launch { print("$snapshot ") } // not 'i', which would race to 3
i++
}
jobs.forEach { it.join() } // prints 0 1 2 (some order)
}go deeper
Recognizes the lambdas 'all print the same number' and that a copy fixes it, without the Ref reasoning.
Explains capture-by-variable vs by-value and applies the val-snapshot fix correctly.
Distinguishes for-loop per-iteration binding from a self-mutated var and ties it to coroutines/callbacks/deferred execution.
Discusses API/design guidance to prevent the class of bug (immutable bindings, passing values, structured concurrency) and reviews for it.
## The core rule Closures capture **variables**, not **values**. When a lambda's execution is **deferred** (stored, returned, run later), it reads the captured variable's value *at run time*, not at capture time. So if several deferred lambdas share one mutable `var`, they all see the same, final value. ## The shared-var pitfall ```kotlin val actions = mutableListOf<() -> Int>() var i = 0 while (i < 3) { actions += { i } // every lambda captures the SAME var i (one Ref) i++ } actions.forEach { print("${it()} ") } // 3 3 3 — all see final i ``` All three lambdas share one `Ref.IntRef`. By the time they run, `i == 3`, so each prints 3. ## Per-iteration snapshot fix Introduce a **fresh `val` per iteration** and capture that: ```kotlin val actions = mutableListOf<() -> Int>() var i = 0 while (i < 3) { val snapshot = i // new binding each iteration actions += { snapshot } // each lambda captures its own val i++ } actions.forEach { print("${it()} ") } // 0 1 2 ``` Each `snapshot` is a distinct read-only binding, so each closure freezes a different value. ## for-loop variables are already per-iteration Kotlin's `for` loop variable is conceptually **re-bound each iteration** (it is read-only inside the body), so capturing it directly usually behaves like the snapshot version: ```kotlin val actions = (0 until 3).map { x -> { x } } actions.forEach { print("${it()} ") } // 0 1 2 ``` This differs from old Java, where capturing a mutable loop counter required an effectively-final temp. The pitfall in Kotlin appears specifically when **you** maintain and mutate your own `var` and capture it across iterations. ## Why it matters in real code This bites with deferred execution: - registering click/event handlers in a loop - launching coroutines (`launch { ... captures loopVar ... }`) where the body runs asynchronously after the loop finished - building a list of callbacks/suppliers If the lambda runs after the loop, a shared mutable var has already moved on. Capture a per-iteration `val` (or pass the value as a parameter) to freeze it. ## Key takeaways - Capture is by variable reference; deferred reads see later mutations. - Shared mutable `var` across closures → all see the final value. - Fix: `val snapshot = current` inside the loop, or pass the value as an argument. - `for`/range loop variables are per-iteration bindings, so they're usually safe.
- Why does capturing a Kotlin for-loop variable directly usually avoid the pitfall?The loop variable is read-only and conceptually re-bound each iteration, so each closure captures a distinct binding rather than one shared mutable var.
- Besides a per-iteration val, what's another clean way to freeze the value?Pass the value as a function parameter — parameters are fresh bindings per call, so the value is captured as the parameter rather than as a shared outer var.
A shared var is a whiteboard everyone reads later; by the time they look, only the last number written remains. A per-iteration val is a photo each person takes of the number when they pass by.
saying these in an interview costs you the question
- Insisting all lambdas print 0 1 2 even when sharing one mutated var
- Saying closures capture the value at the moment of definition
- Believing a val snapshot inside the loop has no effect
- Claiming the bug is identical for for-loop vars and self-mutated vars
- Ignoring that the problem only manifests with deferred execution