An upcoming proposal for simplifying leak free, safe closures.


(James Froggatt) #1

Agreed. Strong capture without an explicit variable binding is unintuitive and generally unsafe. I think it is worth aiming for Swift 3, considering the potential impact.

I have to admit I am quite a heavy user of unowned.
If a change somewhere invalidates my assumptions, I'd rather know about it sooner rather the having things silenty fail. However, the weak strong dance really encourages unowned over weak, even when it may not make be entirely safe.

Just to illustrate the gain to be had:

Before:

{
    doStuff(with: self) //Oh no! I've been silently captured!
}
{ [unowned self] in
    doStuff(with: self) //Probably fine, much more readable.
}
{ [weak self] in
    guard let unwrapped = self else {return}
    doStuff(with: unwrapped) //So much boilerplate! The language must be discouraging this.
}
//unowned seems best

After:

{ [strong self] in
    doStuff(with: self) //Strong capture? No thanks!
}
{ [unowned self] in
    doStuff(with: self) //A capture list‽ Look out, you're doing something unsafe again!
}
{ guard let unwrapped = self else {return}
    doStuff(with: unwrapped) //Oh look, a handy space for a safe unwrap. :slight_smile:
}
//weak seems best

------------ Begin Message ------------
Group: gmane.comp.lang.swift.evolution
MsgID: <75ADA332-2113-4903-89C3-E1DB1692B7B9@me.com>

I may be too late for Swift 3, but I am planning to propose changes to the default behavior for closures capturing object references. The introduction of Swift Playgrounds has raised the importance of simplifying the coding of leak-free, crash-free closures. New developers should not have to understand closure memory management to start writing useful and correct code.

The topic of the closure weak/strong dance has been discussed on this list before. This proposal differs from previous proposals in that it will eliminate the dance altogether by default. I am very interested in hearing others’ opinions as to whether the benefits outweigh the costs of various options.

I have found that Swift’s capture lists and rules are a bit of a mystery to many experienced developers, even though Swift’s closure capture rules are very similar to those of Objective-C. Capture lists are probably thought of as opaque incantations by many new Swift developers if they are aware of them at all. Capture lists should, ideally, not be needed for sensible and safe default behavior.

This discussion is iOS / OS X centric and uses terms from those domains, but these issues should be applicable to almost any codebase that uses closures capturing object references.

Capture lists are required by the fact that object references are captured as `strong` by default, often leading to strong reference cycles and memory leaks.

Use of ‘unowned’
————————
Many examples of using closures capture self as `unowned`. Often, this pattern does not scale well beyond simple examples. iOS and MacOS applications with dynamic UIs, for example, switch between numerous views and view controllers. These objects are dereferenced and reclaimed when they are no longer needed. Closures capturing these objects as `unowned` crash when the references are accessed.

Unfortunately, ‘unowned’ captures are tempting because they eliminate `guard` and `if let` constructs and avoid littering code with optional unwrapping. They are also slightly more performant, but this difference is probably negligible in most application code.

Capturing escaping object references as `unowned` is only safe when object lifetimes are perfectly understood. In complex systems, it is difficult to predict execution order. Even if object lifetimes are perfectly understood when code is originally written, seemingly innocuous changes to complex systems can negate original assumptions.

For these reasons, I believe that capturing object references as `unowned` should be considered an advanced optimization technique.

I now routinely create closures that capture `self` and other object references as ‘weak’ even if I think that I feel that `unowned ` would be safe. This may not be the absolutely most performant solution, but it is straightforward and robust.

The core proposal:
——————

Closures capturing object references should automatically capture all object references as weak.

The closure defined in:

class ClosureOwner2 {
    var aClosure: (() -> Void)?
    
    func aMethod() {
        aClosure = { [weak self] in
          self?.someOtherMethod()
      }
    }

    func someOtherMethod(){}
}

would normally be written as:

        aClosure = {
          self?.someOtherMethod()
        }

Closures that can be optimized to safely capture object references as `unowned` can use the current syntax.

Swift 2 closure without explicit capture lists for object references will not compile.

Capturing strong object references can be very useful in certain circumstances and have a straightforward syntax:

        aClosure = { [strong self] in
          self.someOtherMethod()
        }

Alternatives / Modifications / Improvements(?):
—————————————————————

1) Closures with object references could be simplified further by implicitly including ‘let’ guards for all object references:

        aClosure = {
          // This is included implicitly at the top of the closure:
// guard let strongSelf = self else { return }

            /*strongSelf*/ self.someOtherMethod()
print( “This will not appear when self is nil.” )

… other uses of strongSelf…

        }

This would have the effect of making the execution of the closure dependent upon the successful unwrapping of all of its object references. Object references that are not required to unwrap successfully can be captured as `weak’ if desired.

2) Some of the magic in #1 could be eliminated by introducing a new capture type: ‘required’ to specify ‘weak guarded’ captures, allowing the example closure to be written:

        aClosure = { [required self] in
          self.someOtherMethod()
print( “This will not appear when self is nil.” )
print( “This is not the default behavior and the reason for this is clearly stated.” ) 
        }

This reduces the amount of code required, but may increase the cognitive burden over using the current syntax.

`required` is called `guard` in the “Simplified notation” proposal: https://gist.github.com/emaloney/d34ac9b134ece7c60440

This proposal will differ from the “Simplified notation” proposal in that all object references would be captured as `weak` by default in all closures.

Summary
—————-
Closures with objects references occur frequently in Swift code and the use of closures is probably only going to increase. Current capture list rules are not developer friendly and force developers to deal with subtle asynchronous memory management issues. This increases the cognitive burden particularly for new developers.

Closures should be safe and straightforward by default.

Enhancement #1 without #2 would probably be the easiest option for new developers and result the smallest code size. The question is: is it too “magical” and are the changes in execution behavior going to be significant in practice?

The rules for closure object references with enhancement #1 can be stated succinctly:

By default:

Closures do not affect reference counts of the objects that they reference.
All object references used within a closure must unwrap successfully for the closure to execute.

I believe that these are safe, sensible and understandable rules that will eliminate the need for capture lists for many closures. What do you think?

------------- End Message -------------

From James F