[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:32:43 CST 2015


dispatch_semaphore_wait never returns and thus the current scope is never
left. This is likely what keeps the Photo instance alive.

On Wed, Dec 16, 2015 at 2:24 AM, Marc Knaup <marc at knaup.koeln> wrote:

> 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/7eccba44/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/7eccba44/attachment.jpg>


More information about the swift-evolution mailing list