When does a closure create a cycle, and why doesn’t a lazy var closure always?

3.3.3 · L2 · Interview

Predict first

What does this print?

final class View {
    var onTap: (() -> Void)?
    func go() {}
    init() { onTap = { self.go() } }
    deinit { print("deinit") }
}

var v: View? = View()
v = nil
print("after")
  • deinit, after
  • after

Predict first

What does this print?

final class Cart {
    var price = 2, qty = 3
    lazy var total: Int = { self.price * self.qty }()
    deinit { print("deinit") }
}

var c: Cart? = Cart()
_ = c!.total
c = nil
print("after")
  • deinit, after
  • after

Mental model. A note you read once and discard is harmless. A note you pin to your own door that says “come back to me” is a loop.

A closure that uses self keeps self alive. That is harmless until self also holds the closure. A stored closure property such as var onTap: (() -> Void)? or a handler registered with a long-lived object makes the loop self to closure to self. Break it with [weak self] (or [unowned self] if self must outlive it) in the capture list.

The subtle case is lazy. lazy var total: Int = { self.price * self.qty }() is a closure that is called immediately and thrown away, so it is non-escaping and creates no cycle. But lazy var handler: () -> Void = { self.go() } stores the closure, so it does. Look at whether the closure outlives the statement. Non-escaping closures (map, forEach) never need a capture list.

The stored closure holds self and self holds the closure: a cycle, so deinit never runs.

The closure is called immediately and not stored, so it is non-escaping and cannot form a cycle. The object frees normally.

Common traps

  • Adding [weak self] to every closure. Non-escaping closures cannot cycle, and the noise hides the ones that can.
  • Treating lazy var x = { … }() as a cycle. The immediate call is non-escaping; the stored handler form is the dangerous one.

They might follow up with

  • How do you know a closure escapes? The parameter is marked @escaping, or you assign it to a property.
  • What does guard-let-self do afterwards? It turns the weak capture into a strong local for the rest of the body. See the closures section.

Related questions

  • 1.5.4 What does a capture list like [weak self], [unowned self], [self], or [foo] actually do?
  • 1.5.2 guard let self = self else { return } inside a closure — what does it do to ARC?
  • 3.3.2 Why should a delegate property be weak, and why must the protocol be class-bound?

← Why should a delegate property be weak, and why must the protocol be…   Why does a Timer keep your object alive, and how do you stop it? →