When does a closure create a cycle, and why doesn’t a lazy var closure always?
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? →