[swift-evolution] Proposal: Allow `[strong self]` capture in closures and remove the `self` requirement therein

Marc Knaup marc at knaup.koeln
Tue Dec 15 19:24:24 CST 2015


Waiiiit. That looks more like a bug in Swift than expected behavior which
would us prevent [strong self] from being used.

   - metadata is a value type and thus should be copied. There's should be
   no implicit reference back to the Photo instance.
   - Capturing "self" in the closure because a property is accessed is fine
   and intentional. The reference to self would normally be destroyed as soon
   as the closure is gone.

So something must erroneously keep either the closure alive or hold a
reference to self which it shouldn't.

On Wed, Dec 16, 2015 at 2:12 AM, Drew Crawford via swift-evolution <
swift-evolution at swift.org> wrote:

> Can you explain what is so evil about func evil() when it is called from
> an asynchronously-executed closure?
>
>
> Apologies for earlier brevity, perhaps it would be helpful for me to
> present a more complete, realistic example.  Paste the following into
> main.swift:
>
> import Foundation
>
> print("Hello, World!")
>
> typealias evilArg = [String:String]
> var strongReference: evilArg! = nil
> func evil(foo:evilArg ) {
>     strongReference = foo
> }
>
> final class Photo {
>     var data = [UInt8](count: 100000000, repeatedValue: 0) //a large
> amount of data
>     let metadata: [String: String] = [:] //a small amount of data
>     func save() {
>         dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0)) {
>             evil(self.metadata)
>         }
>     }
> }
> let p = Photo()
> p.save()
> //leaks Photo, data, and metadata
>
> let sema = dispatch_semaphore_create(0)
> dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER)
>
>
> In this example, the memory usage at the end of the program is 100MB:
>
>
> This is because Photo, data, and metadata are all leaked by the evil
> function.
>
> This is the (surprising?) behavior specified in the Swift Book:
>
> [A capture occurs when] the closure’s body accesses a property of the
> instance, such as self.someProperty, or because the closure calls a method
> on the instance, such as self.someMethod(). In either case, these accesses
> cause the closure to “capture” self, creating a strong reference cycle.
>
>
> Even though evil may seem (to the casual programmer who does not read
> language specifications for funsies) like the closure captures only the
> evil argument `metadata`, it **actually** captures *Photo (and therefore
> data)*
>
> The capture of Photo (data) is somewhat clear when we write
>
> evil(self.metadata)
>
> But it is hidden when we write
>
> evil(metadata)
>
>
> As you propose.
>
> I think that if we are going to have semantics that capture Photo (data),
> it had better look like it in a cursory inspection.  The existing syntax is
> not as great as it could be, but it provides a clue.  I think even that the
> existing syntax isn't good enough, because I expect that many people are
> unaware of this particular dark corner.  As you say:
>
> Can you explain what is so evil about func evil() when it is called from
> an asynchronously-executed closure?  I don't see an obvious bug here.
>
>
> It may very well be that there is no *obvious* bug, and so what we may
> actually need is a proposal to make the programmer even more explicit when
> referencing self, not less.
>
> But in any case.  For `strong self` to make sense, we would need to do one
> of two things:
>
> 1.  We could change Swift semantics to only capture metadata in this
> example, however this is a breaking change, and a very very nontrivial
> one.  I do not know why it is specified this way in the first place,
> perhaps a designer can weigh in on that.  I do know that ObjC is very
> similar, so there may be compatibility implications.
>
> 2.  We could introduce additional syntax to provide compiler diagnostics
> to guard in this case, e.g.
>
> func lessEvil(@noescape foo:evilArg ) { //@noescape currently not
> supported for non-closure parameters
>      self.strongReference = foo //should generate diagnostic about
> escaping a @noescape parameter
> }
>
> and then
>
>
> dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0)) { [strong
> self] in
>             evil(metadata) //should generate diagnostic that self is
> required for functions without @noescape
>             lessEvil(metadata) //no diagnostic since parameter is @noescape
> }
>
>
> I think both of these fixes to the proposal are fairly impractical, but
> they would weaken my vote to -0.5.
>
> _______________________________________________
> swift-evolution mailing list
> swift-evolution at swift.org
> https://lists.swift.org/mailman/listinfo/swift-evolution
>
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20151216/9553361a/attachment.html>
-------------- next part --------------
A non-text attachment was scrubbed...
Name: Screen Shot 2015-12-15 at 6.26.52 PM.jpg
Type: image/jpeg
Size: 23371 bytes
Desc: not available
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20151216/9553361a/attachment.jpg>


More information about the swift-evolution mailing list