swiftactorswift-concurrency

Swift Concurrency capturing self weakly in actor


In the example below, when self is captured weakly, Swift raises an error that the member function call needs to be called with await. Expanding the error says:

Calls to instance method 'doSomething()' from outside of its actor context are implicitly asynchronous

Changing the capture of self to strong makes the error go away.

actor MyActor {
    func launchTaskThatDoesSomething() {
        Task { [weak self] in
            guard let self else { return }

            doSomething() // error: Expression is 'async' but is not marked with 'await'
        }
    }

    private func doSomething() {}
}

My question is, what is the difference? Why is the call "outside of its actor context" when self is captured weakly?


Solution

  • First, consider this very simple code

    actor MyActor {
        func f() {
            let x = self
            x.doSomething()
        }
    
        private func doSomething() {}
    }
    

    Swift will claim that x.doSomething() also needs an await. This is because (at the time of writing) Swift does not do data flow analysis to determine that x and self refers to the same actor. As far as Swift is concerned, x could totally be a different instance of MyActor, so calling x.doSomething() potentially involves an actor hop.

    It is a similar situation when you capture self. Swift emits the same error message for:

    Task { [x = self] in
        x.doSomething()
    }
    

    [weak self] is similar. You can think of it as assigning self to some variable of type MyActor? that also happens to be called self:

    Task { 
        [weak `self` = self] in
        self?.doSomething() // same error
    }
    

    In your code, you bound this MyActor? to yet another variable (using guard let), also called self.

    The only exception is [self] or [self = self]. Swift doesn't treat this as assigning self to another variable.

    This behaviour was reported as an issue and is also discussed on the Swift Forums.

    For now, just add await there. At runtime, Swift will know that they are the same actor and will not do an actor hop.