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?
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
.