Explictly capture variables in blocks

Introduction

We propose the explicit declaration of [strong someVar] the same way we declare [weak someVar] when capturing variables in a block scope.
If a "reference type variable" is captured in the block without explicitly declaring it as strong or weak the compiler produces a warning to alert the developer of an unintended strong capture of a "reference type variable".

Motivation

The most common memory-related issue we encounter when working with swift is that variables are implicitly captured in blocks scope when we did not intend them to be strongly captured.

This issue makes the use of blocks so error-prone that we prefer to avoid using it in spite of it being much more readable and more convenient than using delegates.
Consider the following common pattern:

func collectionView(_: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        ...
        cell.didSelectLinkBlock = { [weak self] (link) in
            self?.presentWebViewController(with: link, sender: cell)
        }
        ...
    }

The easily overlooked memory retain cycle is that cell is strongly captured in the block, which is stored in the cell to be called when links are selected, so the cell keeps a reference to itself, creating a memory cycle.

Proposed solution

We are proposing to explicitly declare every variable that is strongly captured in a block to avoid unintentionally strongly capturing variables in the block.
The solution would be applied to the previous example in the following way:

func collectionView(_: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        ...
        cell.didSelectLinkBlock = { [weak self, strong cell] (link) in
            self?.presentWebViewController(with: link, sender: cell)
        }
        ...
    }

Now it is clear that we are capturing cell intentionally, so it is easier to see the memory cycles and avoid it if necessary.

Effect on ABI stability

The proposed solution maintains ABI stability, and the change only produces warnings for reference type variables that are implicitly captured in blocks, which is what we want.

Edit: Feedback points

Some of the great feedback points we got so far include the following:

  • Scaling down the proposal to only produce warnings when reference type variables are implicitly captured in a block.
  • To explicitly declare a strong capture of a reference type we can use the current ability to add the variable to the capture list without adding the strong keyword
  • Only apply the proposal to blocks that are declared as @escaping
  • Not applying the proposal to nested reference type variables
8 Likes

Shouldn’t the closure in your example make an unowned capture of cell? The closure is stored to a property of cell, which means the closure won’t outlive the cell. That seems like a textbook usecase for unowned.

Regardless, I think this proposal would impose an unnecessary degree of verbosity, as well as breaking existing valid source code.

5 Likes

I like this pitch. It would make the programmer think about whether whether to capture the variable strongly, weakly, or weakly as unowned.

1 Like

Hey Nevin thank you for the feedback.

The example was used as an overlooked retain cycle so you are completely right in that cell should be unowned or weak to avoid the cycle. The explicitly declared ‘strong’ should make the developer think about whether it really should be strong

The proposal should not break existing code base and would only produce warnings and not errors. Warnings that would let the developer think about whether the strongly captured variables should really be strongly captured or not.

From our experience in a big project with 20+ developers in different seniority level it is very easy to overlook this kind of memory cycles.

1 Like

We already have that ability, but we don‘t need to type an extra keyword for this.

[weak self, cell] in // `cell` explicitly captured with a strong reference

Is a strong keyword really that necessary?

3 Likes

Reference type is captured as strong by default. If you need weak, you can also write as

func collectionView(_: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        ...
        cell.didSelectLinkBlock = { [weak self, weak cell] (link) in
            self?.presentWebViewController(with: link, sender: cell)
        }
        ...
    }

The Swift language specification is here
“If the type of the expression’s value is a class, you can mark the expression in a capture list with weak or unowned to capture a weak or unowned reference to the expression’s value.”
Excerpt From: Apple Inc. “The Swift Programming Language (Swift 5.1).” Apple Books. ‎The Swift Programming Language (Swift 5.7) on Apple Books

1 Like

Hey Adrian thanks for the feedback.

In our opinion the strong keyword is necessary to make it more apparent if there are memory cycles.

Also, the issue with the current ability is that it doesn't produce a warning if we don't declare the variables that are captured implicitly in the block.

We think that swift should be safe to use in such a way it is harder to make mistakes.

Thanks

2 Likes

I don’t think we use strong anywhere as its implied. It would be a little strange for it to be required only in a capture list.

5 Likes

Well personally I think an extra keyword for each strong element of the list is just visual noise. I really think we should just focus on the main question wether or not Swift should force all captured references to be explicitly written in the capturing list (strong is implied iff no keywords are present).

I‘m not completely sure, but a requirement for a strong keyword could make things harder for anonymous struct syntax as an implicit regression of syntax requirements for captured references.

4 Likes

Hey Danny thank you for the feedback

The issue we are trying to address is exactly the fact that reference type variables are being captured by default as strong since it easily creates retain cycles we want to bring to the developer attention.

If the developer intentionally wants to hold the variable as strong, in order to avoid a warning, the developer would declare it as strong

In our experience using blocks instead of delegates most of the captured variables shouldn't have been captured as strong

1 Like

Have you considered using Instruments and/or Xcode Memory Graph to debug such issues? They are there for a reason.

Since blocks are shown as an anonymous closure in the memory graph tool it is almost impossible to find memory cycles created by strong capturing variables in blocks unintentionally

Perhaps we can improve the tool? It would be great if you can describe in your pitch the challenges you’ve faced when using such tools.

2 Likes

I would prefer this as a rule in linter, not compiler.

This was mentioned in Review capture semantics of self :
"Explicit capture should have an explicit syntax."

Just referencing a variable in closure does not make it explicit to reason about captures. Use a capture list for explicit syntax.

At this point, I would let compiler have implicit captures, and configure explicit capture rules per project.

3 Likes

I personally wouldn‘t mind a compiler/ide flag to disable implicit captures and require them explicitly or error out, similar to how we can treat warnings as errors already.

6 Likes

This kind of feature is something that the Swift team explicitly tries to avoid as a design philosophy:

6 Likes

It is often the case that retain cycles from closures are caused by improper lifecycle handling of the closure. Capturing references as strong is not usually improper, and capturing them weakly often leads to jumping through hoops to ensure the values are non-nil when the body is invoked.

It's better to test for, and debug, retain cycles, than it is to avoid them by playing the weak-strong dance with all your closures. A warning will be cleared by the most expedient method possible, which just means choosing the fixit to add strong to the capture list, and without giving any more thought to retain cycles than before the warning was added.

In short, I don't think any problem will be solved by adding this warning.

8 Likes

Hey Avi thank you for your feedback

Our goal is to bring the implicit capture of variables into the developer attention.

If the developer explicitly wants to ignore the warning and create a retain cycle then he may do it but we believe that indicating a strong capture like in the following example will scream retain cycle:

func collectionView(_: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        ...
        cell.didSelectLinkBlock = { [weak self, strong cell] (link) in
            self?.presentWebViewController(with: link, sender: cell)
        }
        ...
    }

We prefer to make it harder for developers to make mistakes by a default behaviour.

We are bringing up the issue because we see many memory issues caused and passing code reviews by even the most experienced developers oversight

Also, since it will only be a warning then I disagree that the developer will want to apply the quickest fix-it option since there is no fix-it for warnings as far as I know

I appreciate your goal; I just don't think this change would represent forward movement towards achieving it.

Compiler warnings (as opposed to errors), rely on the developer to understand why they are being warned, and rely on the developer having incentive to address the underlying condition which leads to the warning.

In this specific case, the underlying reason is only that they omitted a keyword. The fixit will suggest the keyword, and developers will simply apply the fixit without due consideration of the effects.

One could argue for not including a fixit, but that seems much less likely to pass review. In addition, I have doubts that manually adding strong to silence the warning will lead to any additional consideration by the developer.

I also feel that most references should be captured strongly, and changing the default behavior to require an explicit mention is a step backwards in the conciseness of the language.

6 Likes

Interesting statement, can you please supply some examples of when a reference should be captured strongly in a block that replaces a delegate?

We believe it is so uncommon to strongly capture reference types in blocks that acts as delegates that declaring them explicitly will not effect the conciseness of the language.