- Proposal: TBD
- Author: Evan Maloney
- Status: Draft
- Review manager: TBD
Frequently, closures are used as completion callbacks for asynchronous operations, such as when dealing with network requests. It is quite common to model these sorts of operations in such a way that an object instance represents a request/response transaction, for example:
protocol NetworkTransaction: class
{
enum Result {
case Succeeded(NSData)
case Failed(ErrorType)
}
func execute(completion: (Result) -> Void)
}
Here, the NetworkTransaction
protocol declares the interface by which an asynchronous transaction occurs. The user of a NetworkTransaction
calls the execute()
function, passing in a completion function that is called at some time in the future, when the transaction completes.
For example, imagine a hypothetical DataConsumer
class that uses a transaction to try to fetch some network data and process it:
class DataConsumer
{
let transaction: NetworkTransaction
init(transaction: NetworkTransaction)
{
self.transaction = transaction
}
func fetchData()
{
transaction.execute() { [weak self] result in
guard let strongSelf = self else {
return
}
switch result {
case .Succeeded(let data):
strongSelf.processData(data)
case .Failed(let err):
strongSelf.handleError(err)
}
}
}
func processData(data: NSData)
{
// process the data
}
func handleError(error: ErrorType)
{
// handle the error
}
}
You'll notice the [weak self]
/strongSelf
dance in the fetchData()
function. This is a common pattern with asynchronously-executed closures, and it signals the possibility that a closure might outlive its usefulness.
Because the NetworkTransaction
may complete at any time, it is possible that the closure will execute after the DataConsumer
that initiated the transaction has been deallocated. Perhaps the user has navigated elsewhere in the application and whatever data was to be fetched by DataConsumer
is no longer needed.
In this case, after a DataConsumer
instance goes away, we don't really want the closure doing anything. So, we capture self
weakly to ensure that the closure doesn't hold a reference to the owning DataConsumer
. That prevents a reference cycle and ensures that DataConsumer
can be deallocated when no longer in use.
When it comes time to execute the closure, the guard
statement effectively asks the question, "Is self
still alive?" If the answer is no, the guard forces a return and the rest of the closure does not execute.
If self
is still alive, then the weakly-captured self
will be non-nil
and it will be converted into a strong reference held by strongSelf
for the duration of the closure's execution.
When the closure is done executing, strongSelf
goes away, once again making the DataConsumer
eligible for deallocation when no other references are held.
The [weak self]
/strongSelf
dance requires common boilerplate wherever it is used, and the fact that a self
-like variable with an arbitrary name adds noise within the closure. The more strongSelf
is needed within the closure, the more noise there is.
Further, using a consistent name like strongSelf
is by convention only; it can't be enforced by the compiler, so searching your codebase for a given keyword won't be exhaustive if team members use the wrong name.
The proposed solution adds a new capture type by repurposing the guard
keyword for another use, which would look like:
transaction.execute() { [guard self] result in
switch result {
case .Succeeded(let data):
self.processData(data)
case .Failed(let err):
self.handleError(err)
}
}
Here, the [guard self]
capture list serves as a signal that the compiler should handle the weak/strong dance itself. When encountering [guard self]
, the compiler should emit code that does the following:
- Captures
self
in a weak reference on behalf of the closure - Whenever the closure is about to be executed, the weak reference is checked to see if
self
is still alive- If
self
is not alive, the closure becomes a no-op; calling the closure returns immediately without anything inside the braces being executed - If
self
is alive, it is upgraded to a strong reference for the lifetime of the closure's execution. Within the closure,self
is non-optional, unlike how it would be with a[weak self]
capture. When the closure is done executing, the strong reference will be cleared and only the weak reference will be held on behalf of the closure.
- If
Because guard
is an additional capture type, like weak
and unowned
, it can also be used to capture references other than self
:
let capturingTwo = { [guard self, button] in
// weakly capture self and button
// but execute the closure with strong references
// if and only if self AND button still exist
// when the closure is being asked to execute
}
When encountering multiple references being captured via guard
, the closure will execute only when all references are still alive when the closure is being asked to execute.
Because guard
is a special capture type that causes the closure to become a no-op once a referenced object deallocates, it is only designed to be used with closures returning Void
.
This limitation was deemed acceptable because it would cover the vast majority of cases, and those that it didn't cover can still fall back on the existing technique.
The compiler should emit an error if this notation is used in conjunction with a closure that has a non-Void
return type.
This notation is not intended to be a full-fledged replacement for guard statements within the closure. We are only using guard
here as a way to declare a specific memory-management behavior for references. Therefore, guard
within [
square brackets ]
should be seen as a capture type on par with weak
or unowned
.
Unlike with a typical guard
statement, we are not attempting to support an else
or where
clause, or any boolean expressions within this notation.
Rather, we're simply adding a new capture behavior and providing a means to specify an early exit if the behavior couldn't be fulfilled because one or more objects was deallocated.
The word guard
was chosen as the capture type because (1) it functions as a guard, ensuring that the closure doesn't execute unless the specified objects are still alive, and (2) it obviates the need for the full-fledged guard
statement that would otherwise be required to achieve the same result.
None, since this does not affect any existing constructs. Implementation of this proposal will not result in any code breakage.
The primary alternative is to do nothing, requiring developers to add boilerplate guard code and handle upgrading the weak-to-strong references manually.
As stated above, this leads to needless boilerplate that can easily be factored out by the compiler. Also, the use of a self
-like variable with an arbitrary name makes it more difficult to exhaustively find such uses in large projects. With this proposal, searching for the text "[guard
" is all that's necessary to find all instances of this memory management technique.
Finally, the need to declare and use alternate names to capture values that already have existing names adds visual clutter to code and serves to obscure the code's original intent, making it harder to reason about.
One possible addition to this proposal would extend support to any closure returning an Optional
of some kind.
If one of the objects in a guard
capture list has been deallocated, executing the closure will always result in an immediate nil
return.
This idea was excluded from this iteration of the proposal due to a concern that it relied on a "magic return value" (albeit a reasonable one) and a perception that the community favored a solution with a smaller conceptual footprint.
One possible addition to this proposal would extend support to any closure returning a Bool
.
If one of the objects in a guard
capture list has been deallocated, executing the closure will always result in an immediate false
return.
This idea was excluded from this iteration of the proposal due to a concern that it relied on a "magic return value" (albeit a reasonable one) and a perception that the community favored a solution with a smaller conceptual footprint.
An earlier iteration of this proposal included support for closures with arbitrary return values. The community consensus was that the proposal was too heavy-weight and tried to do too much, and would lead to verbosity within the capture declaration. As a result, this idea was removed from the proposal.
The ability to handle non-Void
return values relied on supporting an else
clause within a guard
-based capture list:
let happinessLevel: () -> Int = { [guard self else -1] in
var level = 0
level += self.isHealthy ? 25 : 0
level += !self.isHungry ? 25 : 0
level += !self.isFearful ? 25 : 0
level += self.hasLove ? 25 : 0
return level
}
Here, the else
clause provides a value to return in cases where self
has gone away and the guard fails.
In this example, if you call happinessLevel()
after self
has been deallocated, the value -1
will always be returned.
Variations on this proposal were discussed earlier in the following swift-evolution threads:
@emaloney what is needed in order to submit this as an official proposal? 🚀