[swift-evolution] Proposal: Change rules for implicit captures due to nested closure captures

Kevin Ballard kevin at sb.org
Tue Dec 8 14:44:43 CST 2015

In Swift code today, when using nested closures, if the inner closure
weakly captures an object (e.g. `self`) that isn't otherwise captured by
the outer closure, the outer closure implicitly strongly captures the
object. This behavior is unlikely to be what the programmer intended,
and results in unwanted object lifetime extension without making it
obvious in the code.

In practice, you'll find this happening in code that looks like

class SomeViewController: UIViewController {    // ...    func
foo(url: NSURL) {        let task =
NSURLSession.sharedSession().dataTaskWithURL(url) { data, response,
error in            let result = // process data
dispatch_async(dispatch_get_main_queue()) { [weak self] in
self?.handleResult(result)            }        }
task.resume()    } }

In here, at first glance it looks like the view controller is being
weakly referenced. But in actuality, the view controller is being
retained for the duration of the background processing, and is then only
converted to a weak reference at the moment where it tries to hop back
on to the main thread. So it's basically the worst of both worlds; the
view controller lives far longer than intended, but it goes away right
at the moment where it could be useful again. It's even worse if the
programmer expected the view controller's deinit() to cancel the network
task, as that can never happen. The fix for this code is to move the
`[weak self]` up to the outer closure, but since the outer closure never
actually touches self directly, it's not immediately obvious that this
is required.

My proposal is to change the rules so that whenever a closure captures
an object only because a nested closure did so, then the outer closure
should capture it using the same ownership semantics (this includes
unowned(unsafe), unowned(safe), weak, and strong). If there are multiple
nested closures that capture it, then we use the following rules:

* If all nested captures use the same ownership, then the outer capture
  uses that ownership.
* If any nested capture is strong, the outer capture is strong.
* If at least one nested capture is weak, and at least one capture is
  unowned or unowned(unsafe), the outer capture is strong. This is
  because there's no (safe) way to convert from weak -> unowned, or from
  unowned -> weak, and we should not crash upon the creation of the
  nested closure, so the outer capture must be strong.
* If at least one nested capture is unowned(unsafe), and at least one
  nested capture is unowned(safe), then the outer capture is

This can be visualized with the following diagram, where the outer
closures uses the right-most node that covers all the children:

.--- weak <-------------------------------.        /
\ strong <                                             + no capture
\                                           /         '--- unowned(safe)
<--- unowned(unsafe) --'

-Kevin Ballard
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20151208/8b78a9af/attachment.html>

More information about the swift-evolution mailing list