A closure uses [weak self] and the owner is released while the work is running. What is the difference between self?.a(); self?.b() and a guard let self at the top?

27.1.3 · L3 · Interview

Predict first

The owner is released 0.1 s into a 0.3 s piece of work. What does this print?

import Foundation

final class Job {
    let name: String
    init(_ n: String) { name = n }
    deinit { print("deinit", name) }
    func step(_ s: Int) { print(name, "step", s) }

    func runOptional(on q: DispatchQueue, g: DispatchGroup) {
        q.async { [weak self] in
            self?.step(1)
            Thread.sleep(forTimeInterval: 0.3)
            self?.step(2)
            g.leave()
        }
    }
    func runGuard(on q: DispatchQueue, g: DispatchGroup) {
        q.async { [weak self] in
            guard let self else { g.leave(); return }
            self.step(1)
            Thread.sleep(forTimeInterval: 0.3)
            self.step(2)
            g.leave()
        }
    }
}

let q = DispatchQueue(label: "worker")
let g = DispatchGroup()
var a: Job? = Job("optional")
g.enter(); a!.runOptional(on: q, g: g)
Thread.sleep(forTimeInterval: 0.1)
a = nil
g.wait()
print("---")
var b: Job? = Job("guard")
g.enter(); b!.runGuard(on: q, g: g)
Thread.sleep(forTimeInterval: 0.1)
b = nil
print("released b")
g.wait()
print("done")
  • optional step 1, deinit optional, ---, guard step 1, released b, guard step 2, deinit guard, done
  • optional step 1, optional step 2, deinit optional, ---, guard step 1, guard step 2, deinit guard, released b, done
  • deinit optional, ---, deinit guard, released b, done

Mental model. Optional chaining is asking “is anyone still home?” before each task and walking away if not. The guard is walking in once and staying until you have finished.

Each self?.method() is its own “turn a weak reference into a strong one for this one call”. Between two such calls nothing holds the object, so if the last owner lets go in between, the object is deallocated and the second call silently does nothing. In the run below the owner drops a while the worker sleeps: “step 1” happens, the object is torn down (“deinit optional”), and “step 2” never prints. The closure still finishes, and the work is half done.

With guard let self the closure turns the weak reference into one strong reference for the whole closure body. Releasing the owner during the sleep only removes the owner’s reference, so “released b” prints, “step 2” still runs, and the object is deallocated when the closure ends, which is why “deinit guard” comes after step 2. Neither is “right”: if the work must be all-or-nothing (a save, a multi-step update), take the strong reference once at the top (Q35); if the work is only worthwhile while the screen exists, per-call optional chaining makes it stop as soon as the screen goes away, but then each step must be safe to skip on its own.

Optional chaining takes a fresh strong reference per call, so the object can die between steps; guard let self holds one for the whole body.

Common traps

  • Mixing a guard for the first step with self?. for later ones. The later steps can still be skipped, giving a half-finished result.
  • Assuming the object is gone the moment its owner drops it. While a closure holds the strong reference from guard let self, the object stays alive, and its deinit runs when the closure ends.

They might follow up with

  • Does this make a retain cycle? No: the closure holds self weakly. The strong reference only lives for the duration of the closure body.
  • How would you do the first step on the main thread and the second later? Re-bind a strong reference at each hop, and decide at each hop whether it is still worth continuing.

Related questions

  • 27.1.2 What do closures print when they capture a loop variable, a shared variable, or a copy in a capture list?
  • 27.1.1 In what order do initialisers and deinitialisers run for a subclass with stored properties, and what does…
  • 27.3.3 A screen stores a DispatchWorkItem whose closure uses self. Does cancelling the item fix the leak, and what…
  • 1.5.2 guard let self = self else { return } inside a closure — what does it do to ARC?
  • 3.3.3 When does a closure create a cycle, and why doesn’t a lazy var closure always?
  • 3.3.6 Can a Task cause a retain cycle, and how do you avoid it?

← What do closures print when they capture a loop variable, a shared…