I think this is a bug, but I would like confirmation. The following code compiles cleanly, whereas I think it should error due to lack of explicit self
final class Foo {
var thisVariableInvolvesSelf = 42
func leak1() {
Task {
thisVariableInvolvesSelf += 1
}
}
}
In the Swift book, it says that escaping closures require an explicit self:
If you want to capture self , write self explicitly when you use it, or include self in the closure’s capture list.
I believe Task {} is actually the following constructor which takes an @escaping parameter
As a consequence of this behavior, in the following example, the closure strongly retains self even though [weak self] is in the capture list. As a corollary, the function leaks.
final class Bar {
var thisVariableInvolvesSelf = 43
func leak2() {
Task { [weak self] in
//never yields
await withCheckedContinuation({(_: CheckedContinuation<(),Never>) in })
thisVariableInvolvesSelf += 1
self?.innocent()
}
}
func innocent() { }
}
This leak has caused me some headache. I do not really understand the mechanism of it, unless I there is some alternative constructor for Task which takes a nonescaping closure.
True enough, though I dislike conflating leaks with extended reference lifetimes, since the lifetime would end given the rest of the code is executed. In any case, does a newer toolchain affect the behavior at all? I wonder if this is a bug in 5.5 that was fixed later.
Looks like this issue with @_implicitSelfCapture was anticipated by @Douglas_Gregor in the review of SE-0304:
Explicit self is there for a specific purpose: to help identify retain cycles by requiring one to explicitly call out self capture where it's likely to cause retain cycles. We intentionally did not make it required everywhere , and then later we removed explicit self in other unlikely scenarios . When capturing self in a task, you effectively need the task to have an infinite loop for that capture to be the cause of a reference cycle. That fits very well with the direction already set for explicit self .
Though perhaps the necessity of having infinite loops in Tasks was not as anticipated yet since AsyncSequence wasn't fully established. But since throwing off a task to handle an infinite sequence will be rather common, it certainly appears this point needs to be reexamined.
At the very least, allowing implicit capture even after the user has initiated manual capture seems like a bug, so that should be filed.
I also request "requiring explicit self capturing" on Task closure.
Today I stumbled on a difficult bug and the root cause was unintentionally (implicitly) captured self in Task closure. Retain cycle or "unexpectedly prolonged lifetime" is one of the hardest, time-consuming bug to find out and fix in RC system.
On Swift, I can forget the risk of implicitly captured self because compiler checks it for me. If implicit self capturing is allowed, I cannot forget about it anymore. Getting more things to care and remember is really painful experience. Please consider this pain more seriously.
I had this problem today. Implicit self seems OK for async work with an expected ending, but a nonterminating AsyncSequence is now an attractive way to subscribe a view controller to data changes. You'd want to cancel the task in deinit, so avoiding strong capture is key.
I noticed this detail as well: If you capture self weakly, but then you convert to a strong reference and hold that across a suspension point, you reintroduce the retain cycle for the duration. Is there guidance about avoiding guard let self in async closures, or just in unbounded for awaits?
I don't think cancelling on de-init is the way to go. Instead, copying the behavior of SwiftUI.View.task decouples the lifetime of the UIView object and the task. That is, starting the task before the view appears and cancelling it after it disappears.
For a task that only matters while the VC is displayed, I would agree.
How about this case: A view controller is a child of a tab bar controller. It uses an AsyncSequence-consuming for await in a Task to listen to notifications indicating it should invalidate its data. After the VC receives invalidation from the task, it loads its data again immediately if visible; otherwise it loads when it next appears. Here the notification-observing task needs to outlive visibility but not outlive the view controller. If the whole tab bar controller is modally dismissed, the view controller should not leak.
Avoiding strong self capture is the usual way to solve that kind of thing, but we don't have the compiler's encouragement in this case.
This strikes me as a problem somewhat different shape than the one that the implicit self capture rules are meant to solve. Those diagnostics are focused on trying to prevent common causes of strong reference cycles, namely, an instance of a reference type which (perhaps transitively) holds on to a strong reference to a closure which captures the original instance.
But the problem where the lifetime of captured state can get extended indefinitely in a non-terminating Task persists quite apart from any formal reference cycles. Indeed, this issue would persist even if you never held on to the returned Task object at all because it's the internal runtime machinery that keeps an executing Task alive, not the fact of any strong reference being held onto.
As you already note, the typical guard let self dance used to typically satisfy the compiler's complaints about self captures wouldn't be a sufficient mitigation here—you'd need to make sure you are dropping your strong self reference through every iteration of the loop. This has been discussed before here and here at the very least.
I think you're right that this pattern deserves better diagnostics but I'm not sure that simply falling back on the usual self capture rules are the right tool for the job.