Hey folks, this pitch discusses introducing a new guard
capture specifier for closure capture lists. This topic has come up before over the years ([1] [2] [3] [4] [5]), but has never been implemented before as far as I can tell (until now!).
This proposal is implemented in apple/swift#38718.
Introduction
guard
captures behave like weak
captures (e.g. guard
captures do not retain the captured value), but the closure body is only executed if the captured objects still exist.
class ViewController {
let button: Button
func setup() {
button.tapHandler = { [guard self, guard button] in
if canBeDismissed(by: button) {
dismiss()
}
}
}
}
Motivation
In some classes of Swift programming, such as UI programming, closures are a predominant way to perform action handling and send event notifications between multiple objects. When passing closures between parent objects and their children, special care must be taken to avoid retain cycles.
weak
captures are often the preferred method to avoid retain cycles. In action handling and event notification closures, there is typically no work to perform if self
(or other captured objects) no longer exist. Because of this, a large number of these closures simply guard
that the weakly captured object still exists, and otherwise do nothing.
For example, this sample code weakly captures self
and button
:
class ViewController {
let button: Button
func setup() {
button.tapHandler = { [weak self, weak button] in
guard let self = self, let button = button else { return }
if self.canBeDismissed(by: button) {
self.dismiss()
}
}
}
}
This is a very common pattern, but has some notable drawbacks:
-
weak
captures don't support implicitself
.- As of SE-0269, strong and
unowned
captures ofself
enable implicitself
calls within the body of escaping closures. This is not straightforward to support forweak
closures in the general case, and was intentionally excluded from SE-0269.
- As of SE-0269, strong and
-
guard let value = value else { return }
is quite a bit of boilerplate in this context.- Closure syntax is lightweight, but the
guard
statement for a correspondingweak
capture is relatively heavy. - In the example above the
guard
statement (which is effectively boilerplate) is more code than the rest of the closure combined.
- Closure syntax is lightweight, but the
-
unowned
captures are not necessarily a suitable alternative.-
unowned
captures can also be used to prevent retain cycles, but will cause the application to crash if executed after the captured object has been deallocated. - Crashing on invalid access is memory-safe, but is often considered undesirable from the perspective of application stability.
- Avoiding crashes with
unowned
requires non-local reasoning. It is impossible to know whether or not the captued objects will still exist when the closure is called without reading all code that can potentially call the closure.
-
Proposed solution
We should introduce a new guard
capture specifier:
class ViewController {
let button: Button
func setup() {
button.tapHandler = { [guard self, guard button] in
if canBeDismissed(by: button) {
dismiss()
}
}
}
}
guard
captures behave like weak
captures (e.g. guard
captures do not retain the captured object), but the closure body is only executed if the captured objects still exist.
When using [guard self]
, self
is always non-nil in within the closure body. Following the precedent of SE-0269, implicit self
will be permitted within closures that use [guard self]
.
Detailed design
guard
captures desugar into a weak
capture with a corresponding guard let value = value else { return }
statement. This guarantees that the closure body, as written, will only be executed if the captured calue is non-nil.
button.tapHandler = { [guard self, guard button] in
// ...
}
// is desugared into:
button.tapHandler = { [weak self, weak button] in
guard let self = self, let button = button else { return }
// ...
}
guard
captures are only permitted in closures that return Void
. Attempting to use guard
captures in a closure that does not return Void
will result in an error with the following fixit:
// š `guard` capture specifiers are only permitted in closures that return `Void`
// FIXIT: Change `guard` to `weak` and insert `guard let self = self else { return <#value#> }`
var canBeDismissed: () -> Bool = { [guard self] in
return true
}
Source compatibility
This proposal is purely additive, and has no effect on source compatibility.
Effect on ABI stability
This proposal desugars into existing syntax, so has no effect on ABI stability.
Effect on API resilience
After being parsed, guard
captures are treated as weak
closures. This proposal has no visible effects on the public API of closures. Changing a weak
capture to a guard
capture, and vice versa, is always permitted and has no effects on public API.
Alternatives considered
Support closures that return a value
In this proposal, guard
captures are only permitted in closures that return Void
. To support guard
closures for closures that return an actual value, we would have to pursue directions like:
- provide a default value that is returned if the captured objects no longer exist (e.g. return
nil
for closures that return anOptional
). - provide a way to specify the return value in the capture list (like
[guard self else nil]
, as a strawman syntax).
These sorts of designs either promote hidden behaviors, or would require new syntax that is not meaningfully better than the status quo.