Allow implicit `self` for `weak self` captures, after `self` is unwrapped

As a follow up from this discussion earlier this year, here's a pitch for allowing implicit self for weak self captures. This is implemented in apple/swift#40702, and the full proposal document is here.

Introduction

As of SE-0269, implicit self is permitted in closures when self is written explicitly in the capture list. We should extend this support to weak self captures, and permit implicit self as long as self has been unwrapped.

class ViewController {
    let button: Button

    func setup() {
        button.tapHandler = { [weak self] in
            guard let self = self else { return }
            dismiss()
        }
    }

    func dismiss() { ... }
}

Motivation

Explicit self has historically been required in closures, in order to help prevent users from inadvertently creating retain cycles. SE-0269 relaxed these rules in cases where implicit self is unlikely to introduce a hidden retain cycle, such as when self is explicitly captured in the closure's capture list:

button.tapHandler = { [self] in
    dismiss()
}

SE-0269 left the handling of weak self captures as a future direction, so explicit self is currently required in this case:

button.tapHandler = { [weak self] in
    guard let self = self else { return }
    self.dismiss()
}

Since self has already been captured explicitly, there is limited value in requiring authors to use explicit self. This is inconsistent, and adds unnecessary visual noise to the body of closures using weak self captures.

Proposed solution

We should permit implicit self for weak self captures, once self has been unwrapped.

This code would now be allowed to compile:

class ViewController {
    let button: Button

    func setup() {
        button.tapHandler = { [weak self] in
            guard let self = self else { return }
            dismiss()
        }
    }

    func dismiss() { ... }
}

Detailed design

Like with implicit self for strong and unowned captures, the compiler will synthesize an implicit self. for calls to properties / methods on self inside a closure that uses weak self.

If self has not been unwrapped yet, the following error will be emitted:

button.tapHandler = { [weak self] in
  // error: explicit use of 'self' is required when 'self' is optional,
  // to make control flow explicit
  // fix-it: reference 'self?.' explicitly
  dismiss()
}

Like in SE-0269, the innermost closure most capture self explicitly in order to use implicit self.

execute { [weak self] in
  guard let self = self else { return }

  execute {
      // call to method 'operation' in closure requires explicit use of 'self' to make capture semantics explicit
      dismiss()
  }
}

Alternatives considered

It is technically possible to also support implicit self before self has been unwrapped, like:

button.tapHandler = { [weak self] in
    dismiss() // as in `self?.dismiss()`
}

That would effectively add implicit control flow, however. dismiss() would only be executed when self is not nil, without any indication that it may not run. We could create a new way to spell this that still implies optional chaining, like ?.dismiss(), but that is not meaningfully better than the existing self?.dismiss() spelling.

13 Likes

With all the special handling of self in captures, I wonder whether it should also be given additional restrictions. This code, for example, would be legal but confusing:

class C {
  static var global: C? = C(name: "global C")
  let name: String
  func identify() { print("hello I am \(name)!") }
  func identifyLater() {
    Task { [weak self] in
      guard let self = self ?? C.global else { return }
      identify() // What does this print?
    }
}

Perhaps it should be illegal to re-bind self to anything but the result of successfully unwrapping weak self in a closure that captures self.

3 Likes

Good point -- implicit self shouldn't be allowed in that case.

SE-0269 only allows implicit self when captured directly, so we should follow that precedent here.

This new rule would only apply when self is captured directly, and with the name self . This includes captures of the form [self = self] but would still not permit implicit self if the capture were [y = self] . In the unusual event that the user has captured another variable with the name self (e.g. [self = "hello"] ), we will offer a note that this does not enable use of implicit self (in addition to the existing error attached to the attempted use of implicit self ):

Note: variable other than 'self' captured here under the name 'self' does not enable implicit 'self'.

Just thinking here, but self is captured automatically when used within a closure.

.onReceive { value in
    self.doSomething(with: value)
}

Would be nice if weak self too could be automatically captured, depending upon usage in the closure. e.g.

.onReceive { value in
    self?.doSomething(with: value)
}

Using the optional version of self would be the same as doing.

.onReceive { [weak self] value in
    self?.doSomething(with: value)
}

This would avoid a LOT of typing and boilerplate code when using Combine and RxSwift and other closure-heavy systems.

1 Like

It’s a bad idea to repeatedly use a weak reference to self, though. It could go to nil in the middle of executing your function. Immediately strongifying weak self and keeping it strong for the duration of the closure is much less likely to lead to hard-to-debug concurrency issues.

1 Like

Blockquote It’s a bad idea to repeatedly use a weak reference to self , though.

True, but if I'm maintaining a weak self I tend to do what was shown in the example and use a single call out of the closure.

.onReceive { [weak self] value in
    self?.doSomething(with: value)
}

If a closure contains a mixture of self?. and self. accesses, how should the compiler react?

Should self?. really imply weak self when TSPL specifically contains such strong guidance in favor of unowned?

If the captured reference will never become nil , it should always be captured as an unowned reference, rather than a weak reference.

(emphasis added)

1 Like

I updated the proposal and implementation to include this, thanks for the suggestion.

Following SE-0269, implicit self will only be permitted if the self optional binding specifically, and exclusively, refers the closure's self capture:

button.tapHandler = { [weak self] in
   guard let self = self ?? someOptionalWithSameTypeOfSelf else { return }

   // error: call to method 'method' in closure requires explicit use of 'self' 
   // to make capture semantics explicit
   method()
}
1 Like

While this is a cool idea, I kinda wish we could tackle a more general idea of rebinding self.

2 Likes