@MainActor
protocol P: Sendable {}
@MainActor
class C {
let p: any P
nonisolated init(p: any P) {
self.p = p
}
}
While this does not?
@MainActor
protocol P {}
@MainActor
class C {
let p: any P
nonisolated init(p: any P) {
self.p = p // Main actor-isolated property 'p' can not be mutated from a nonisolated context
}
}
Isn't an actor-isolated type already Sendable by definition?
According to my knowledge, this does not apply to protocols. Global actor isolated protocols just means the requirements need to be isoalted to that actor, not the type itself:
@MainActor
protocol P1 {
var a: Int { get set }
}
// Means:*
protocol P1 {
@MainActor var a: Int { get set }
}
For example:
@globalActor
actor DatabaseActor {
static let shared = DatabaseActor()
}
@MainActor
protocol P1 {
var a: Int { get set }
}
@DatabaseActor
protocol P2 {
var b: Int { get set }
}
final class P1P2Conformer: P1, P2 {
var a: Int = 1 // isolated to MainActor
var b: Int = 2 // isolated to DatabaseActor
var c: Int = 2 // nonisolated**
}
*: There is a small difference between the two. The first one - if there are no other protocols like this that are isoalted to a different global actor - will make the entire type isolated to it. The second doesn't.
**: Normally the isolation would've been inherited from the conformance and make this also isolated. But since there are 2 different isoaltion domains the compiler makes it nonisoalted.
Also the requirements can be satisfied with nonisolated members:
nonisolated final class NonisolatedP1P2Conformer: P1, P2 {
var a: Int = 1
var b: Int = 2
}
The protocol P itself does not become isolated to @MainActor. All it does is make any members of P@MainActor isolated by default, but not the protocol itself. Ms. Borla already gave an excellent answer in this thread: Global actor isolated protocols and SendableMetatype - #2 by hborla
Make protocol P inherit from Sendable if you want it to be sendable.
Even if it's sendable, @MainActor still ensures its properties (aka its members) are main-actor isolated.
This example should help clarify:
@MainActor
protocol P: Sendable {
var x: Int { get set } // @MainActor applies here
}
@MainActor
class C {
let p: any P
let x: Int
nonisolated init(p: any P) {
self.p = p
self.x = p.x // ❌ Main actor-isolated property 'x'
// can not be referenced from a nonisolated context
}
}
Main actor-isolated property 'p' can not be mutated from a nonisolated context
or
Main actor-isolated property 'x' can not be referenced from a nonisolated context
are misleading. It's possible to set property x in your example in a nonisolated init. This compiles:
@MainActor
protocol P: Sendable {
var x: Int { get set } // @MainActor applies here
}
@MainActor
class C {
let p: any P
let x: Int
- nonisolated init(p: any P) {
+ nonisolated init(p: any P) async {
self.p = p
- self.x = p.x
+ self.x = await p.x
}
}
A tangentially related question (it uses a similar setup as OP's example, but not about OP's question). This doesn't compile. It's as expected because a function in another isolated domain is allowed to call this nonisolated init.
class NS {}
@MainActor
class C {
let ns: NS
nonisolated init(ns: NS) {
self.ns = ns
}
}
But this doesn't compile either. It that by design?
class NS {}
@MainActor
class C {
let ns: NS
- nonisolated init(ns: NS) {
+ nonisolated init(ns: sending NS) {
self.ns = ns
}
}
EDIT: on a second thought, I'm not sure about the second example. Is it OK for a nonisolated synchronous function to have sending parameters?
Another observation. It's well known that an existential value any P doesn't conform to P. So when I saw this code, I wasn't sure if it would compile.
protocol P: Sendable {}
@MainActor
class C {
let p: any P
nonisolated init(p: any P) {
self.p = p
}
}
It compiles. I can understand it because Sendable is a mark protocol. Just curious, does it mean compiler has implemented a hardcoded exception for this?