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 implicit self.
- As of SE-0269, strong and
unowned captures of self enable implicit self calls within the body of escaping closures. This is not straightforward to support for weak closures in the general case, and was intentionally excluded from SE-0269.
-
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 corresponding weak 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.
-
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 an Optional).
- 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.