swiftactorswift-concurrency

Why does a @MainActor nonisolated property accessing a @MainActor protocol’s nonisolated property produce a warning?


I’m encountering an error when mixing @MainActor and nonisolated, particularly when a @MainActor nonisolated property accesses a nonisolated property from a @MainActor protocol. The code is

Example 1

@MainActor
protocol Foo {
    nonisolated var value: Int { get }
}

@MainActor
class Bar {
    nonisolated var value: Int {
        // Warning: Main actor-isolated property 'foo' cannot 
        // be referenced from a nonisolated context.
        foo.value 
    }
    
    let foo: Foo
    
    init(foo: Foo) {
        self.foo = foo
    }
}

The warning is: Main actor-isolated property ‘foo’ cannot be referenced from a nonisolated context; this is an error in the Swift 6 language mode.

However, when I change the protocol to a class, or when I remove the nonisolated from the Bar class property, the error is gone.

Example 2: Changing the protocol to a class

@MainActor
class Foo {
    nonisolated var value: Int { 1 }
}

@MainActor
class Bar {
    nonisolated var value: Int {
        foo.value // No warning: 'foo' is a class instance.
    }
    
    let foo: Foo
    
    init(foo: Foo) {
        self.foo = foo
    }
}

Example 3: Removing nonisolated from the Bar class property

@MainActor
protocol Foo {
    nonisolated var value: Int { get }
}

@MainActor
class Bar {
    var value: Int {
        foo.value // No warning when the property is isolated.
    }
    
    let foo: Foo
    
    init(foo: Foo) {
        self.foo = foo
    }
}

Question:

Is this behavior expected? What exactly causes this warning when dealing with a protocol?


Solution

  • The difference a protocol makes, is that the compiler cannot reason about how a property requirement is implemented.

    As your class Foo example shows, if the property is implemented as a computed property, then everything is okay. But what if the property is a stored property, and is a var? I can write such an implementation:

    // this class is non-isolated!
    class FooImpl: Foo, _RemoveGlobalActorIsolation {
        var value: Int = 0
    }
    

    Someone could write to foo.value on the main actor, at the same time when your code is reading foo.value from a non-isolated context, and cause a data race.

    The takeaway here is that @MainActor protocols are not necessarily implemented by @MainActor types! This is similar to how a var stored property can be used to fulfil a { get } protocol requirement, even if it also has a setter. A @MainActor protocol requires implementations to be able to work on the main actor, but non-isolated types can work anywhere, including on the main actor!

    In other words, the @MainActor on a protocol is not really a requirement for protocol conformers. It requiring the consumers of the protocol to only use it on the main actor. The conformer can "take advantage" of that and use @MainActor APIs, but it is totally optional.

    _RemoveGlobalActorIsolation is a built-in SwiftUI protocol isolated to a different global actor, and by conforming to multiple protocols isolated to different global actors, I declared a non-isolated class. In Swift 6.1, you can directly write nonisolated class FooImpl: Foo (SE-0449).

    Removing nonisolated from Bar.value works, because the compiler can now reason that all accesses to Bar.foo will occur on the main actor.

    Side note: there is no such thing as @MainActor nonisolated. Nothing can be both isolated to an actor, and be non-isolated at the same time. Bar.value is simply nonisolated - it is not @MainActor.