Part IICode reviewWhat does this print? · 03 of 2915 min

03 · What does this print? — closures and values

Value vs reference semantics, capture lists, loop captures and defer — five lines of output to predict.

Shape: what does this print · Reported: closure capture and escaping questions at Revolut and Delivery Hero · Verified: run with Swift 6.4

"Line by line — what prints?"

struct Counter { var value = 0 }
var a = Counter()
var b = a
b.value += 1
print(a.value, b.value)                          // ①

var x = 1
let byReference = { print("ref", x) }
let byValue = { [x] in print("value", x) }
x = 2
byReference()                                    // ②
byValue()                                        // ③

final class Box { var value = 0 }
let box = Box()
let snapshot = { [box] in print("box", box.value) }
box.value = 5
snapshot()                                       // ④

var handlers: [() -> Void] = []
for i in 0..<3 { handlers.append { print("loop", i) } }
handlers.forEach { $0() }                        // ⑤

func f() -> Int {
    var n = 1
    defer { n += 10; print("defer", n) }
    n += 1
    return n
}
print("returned", f())                           // ⑥
A hint, if you're stuck
  • Is Counter a value type or a reference type? What does b = a copy?
  • A closure without a capture list sees the variable. With [x] it takes a copy — but a copy of what, for a class?
  • When exactly does defer run, compared with return?
Answers (verified output)
0 1
ref 2
value 1
box 5
loop 0
loop 1
loop 2
defer 12
returned 2
  • ① Structs are values: b = a copies. Changing b leaves a alone.
  • ② A closure captures the variable, not its value at the time. It sees x = 2.
  • ③ A capture list copies the value when the closure is created: 1.
  • ④ The trap. [box] copies the reference. It's still the same object, so it sees 5. Capture lists snapshot values, not objects.
  • ⑤ Each loop iteration has its own i, so 0 1 2. (In some other languages this prints 3 3 3; saying so shows you know why Swift doesn't.)
  • ⑥ return n evaluates 2 first, then defer runs and changes n to 12 — too late to affect the returned value. Prints defer 12 before returned 2, because f() must finish before the outer print can.
The idea behind it

Swift has two kinds of types. A value type (a struct or an enum) behaves like a photocopy: assign it and you get an independent copy. A reference type (a class) behaves like a house address: copy it and you have two slips of paper pointing at the same house. Paint the house through one slip and the other slip sees the new colour.

Closures capture the outside variables they use. By default they capture the variable itself, so they see any later change to it. A capture list like [x] takes a copy at the moment the closure is created. But copying a class reference copies the address, not the house — so the closure still sees changes made to the object. That is exactly why [weak self] works: it changes how the closure holds the address (without keeping the house standing), not what's inside.

defer runs when the function is on its way out — after the return value has already been worked out. Changing a variable in defer can't change what was returned.

How to make it unsurprising
let box = Box()
let snapshot = { [value = box.value] in print("box", value) }
box.value = 5
snapshot()                                   // box 0
  • To freeze an object's state, capture the value, not the object. [value = box.value] copies the Int when the closure is made; [box] only copies the address.
  • Write the capture list on purpose whenever a closure outlives the line it's on, so the reader sees what's a snapshot and what's live.
  • Use defer for clean-up only — closing, unlocking, resetting. Never to change a value you return.
What I'm really scoring

④ separates people who memorised "capture lists copy" from people who understand that a class reference is itself the value being copied. That same understanding is what makes [weak self] make sense — you're choosing how the closure holds the reference, not copying the object.

What I'd ask next
  • "What makes a closure escaping, and why does Swift make you say so?" — It can be called after the function returns (stored, or passed to async work). Escaping closures can create retain cycles and need explicit self; non-escaping ones can't outlive the call, so the compiler can optimise them and lets you omit self. Since Swift 5.3 (SE-0269) you can also omit it in an escaping closure that captures [self] explicitly, or when self is a struct or enum.
  • "When is [weak self] unnecessary?" — When the closure isn't stored by anything self owns: UIView.animate, map, a one-shot request you're happy to let finish.
  • "weak vs unowned?" — unowned crashes if the object is gone; use it only when the closure provably can't outlive the object.
  • "Change Counter to a class — what does ① print?" — 1 1: both names point at one object.

Sources for this chapter