Weak Self -- Closure Rules of Thumb
In Swift, you can weak-ify references to self
in escaping closures, and then you need to deal with the case that the reference is gone when the block is called.
Last month, Benoit Pasquier and Chris Downie presented different takes on the problem. That discussion was excellent. It prompted me to take some more time to revisit this problem systematically, and I took away a couple of notes for future-me.
The three popular options as Chris listed them are these:
- Do not capture strong reference
- Override
self
with strong reference - Bind
strongSelf
As always, either way has its pitfalls.
I’ve used all approaches in my own and client apps.
But I do prefer (2), to use [weak self] in
for outer and inner closures, with guard let self = self else { return }
early on because this can symmetrically and consistently be used everywhere: It doesn’t matter if it’s the inner or outer closure. You can do this habitually, with code completion or templates, and write static code analyzers to catch that in PRs. These are all benefits in my book.
The dance with strongSelf
creates a parallel set of problems and can make the inner closure’s setup depend on the outer closure I would like to avoid because I know I’m too stupid to get this correct 100% of the time.
See the rundown below for details.
1. Do not capture strong reference
Use self?.foo
everywhere – but self
can become nil in the middle of the block.(via Chris Downie)
Might not be what you want to use outside of one-liners.
2. Overriding self
guard let self = self else { return }
override the local weak, optional self
reference with the strong, non-optional one.
- Benoit’s point: You can forget
weak
ifying inner closures and accidentally create a retain cycle again! - Can arguable make it a bit harder to mess this up when you do it consistently, like a machine. (See 3. below.)
Problematic example adapted from Benoit:
self.doSomething = { [weak self] in
guard let self = self else { return }
self.doSomethingElse = { // ⚠️ forgot [weak self], now there's a cycle!
self.foo()
}
}
self.doSomething()
Could potentially be detected by static analyzers and SwiftLint.
3. Capture in e.g. strongSelf
guard let strongSelf = self else { return }
– Bind strong self reference inside the block’s scope with a new name.
Swift compiler doesn’t warn you if you accidentally bind strongSelf
from outside. Example by Chris
firstChild.playLater { [weak self] in
guard let strongSelf = self else { return }
strongSelf.gamesPlayed += 1
strongSelf.secondChild.playLater {
if let strongSelf = self {
// 👍 Locally bound the weak self reference.
// (But didn't use the bound variable.)
print("Played \(self?.gamesPlayed ?? -1) with first child.")
}
// ⚠️ Strongly captures `strongSelf` from outside by accident
// and creates cycle.
strongSelf.gamesPlayed += 1
completion(strongSelf.gamesPlayed)
}
}
To mitigate, make sure to always
guard let strongSelf = self else { return }
at the beginning of a block. Could be detected by static code analyzers.
But this is too clever for my taste: in the example above, you don’t need to pass in any weak reference. The outer block weak-ifies the reference to self
already, and that’s enough. Then the strongSelf
reference lives next to it and creates a parallel set of problems. – Instead, I favor making the same mistake in all places and consistently apply [weak self] in
.
You can of course rewrite this to require [weak self]
in the inner closure, too. Doesn’ hurt (I just tried), but is also not necessary to get a weak reference in the inner closure.
Chris’s rules summarize this nicely:
- Only use a strong
self
for non-@escaping
closures (ideally, omit it & trust the compiler)- Use
weak sel
f if you’re not sure- Upgrade
self
to a strongly-retainedself
at the top of your closure.