Nested weak capture and implicit self in Swift 6

The following code compiles fine in -swift-version 5, but errors with implicit use of 'self' in closure; use 'self.' to make capture semantics explicit in -swift-version 6

func acceptsClosure(_ x: @escaping () -> Void) {}

@MainActor
final class A {
    func nonisolatedMethod() {}

    func createsNestedClosure() {
        acceptsClosure { [weak self] in
            Task { @MainActor [weak self] in
                guard let self else { return }
                nonisolatedMethod()
             // ^ implicit use of 'self' in closure; use 'self.' to make capture semantics explicit
            }
        }
    }
}

If I remove either the outer [weak self] it accepts the code, but I am unsure of the reference capture semantics here (am I good? Is the outer capture not going to retain self?)

Is this the intended behavior, or is this a bug?

2 Likes

SE-0365 offers an official solution, but it seems nonsensical to have to bother with reweakifying. It all apparently went live with Swift 5.8, so hopefully it's a regression and what you posted is actually intended to work as written.

func createsNestedClosure() {
  acceptsClosure { [weak self] in
    guard let self else { return }
    Task { @MainActor [weak self] in
      guard let self else { return }
      nonisolatedMethod()
    }
  }
}

In your original version, with two [weak self] captures, the following happens when the closure is called:

  • If self is nil at this point:

    • A Task is created, weakly capturing self.
    • The Task is immediately executed
    • The task immediately returns through the guard, as self is nil.
  • If self is not nil at this point:

    • A Task is created, weakly capturing self.
    • The Task is immediately executed.
    • The task immediately captures self strongly through the guard.
    • self.nonisolatedMethod() is called (which would also capture self strongly!).
    • The task finishes and self is released.

You probably want to remove the inner [weak self] in, particularly if the first thing the Task is doing is guard let self else { return }:

func acceptsClosure(_ x: @escaping () -> Void) {}

final class A: @unchecked Sendable {
    func nonisolatedMethod() {}

    func createsNestedClosure() {
        acceptsClosure { [weak self] in
            guard let self else { return }
            Task { @MainActor in
                nonisolatedMethod()
            }
        }
    }
}

The stored closure will have a weak reference to self until it's executed. Then, one of two things happen:

  • If self is nil at this point:

    • The closure returns through the guard without creating a Task.
  • If self is not nil at this point:

    • A Task is created, strongly capturing self.
    • The task is immediately executed.
    • self.nonisolatedMethod() is called.
    • The task finishes and self is released.

This is usually what you want, because a Task should eventually finish, so you're not creating a retain cycle by having a Task strongly capture self[1]. And in this case, the Task was already strongly capturing self immediately either way.

This thread touches on this topic too, it's worth a read.


  1. Key exception: if the Task can run forever. For example, by containing a for await loop over a never-ending AsyncSequence. ↩︎

cc @cal—the fact that Harlan's original code doesn't work in Swift 6 mode seems like a bug, no?

Definitely looks like a bug to me

2 Likes

Here's a fix: Fix issue where implicit self was unexpectedly not allowed in nested weak self closure in Swift 6 mode by calda · Pull Request #78738 · swiftlang/swift · GitHub thanks for the report!

4 Likes