I have this code:
actor Actor {
@MainActor
var checker: @MainActor @Sendable () -> Void = {}
init(checker: @escaping @Sendable () -> Void) {
self.checker = checker
_ = self.checker // access it in nonisolated context
}
nonisolated func noniso() {
Task { @MainActor in
_ = self.checker
}
}
}
My understanding is that, the init is nonisolated, because I can create the actor in nonisolated context:
let actor = Actor {}
However, in the nonisolated function noniso
, I have to use a @MainActor
in order to access the self.checker
. Otherwise there will be compiler error.
Why can I access this isolated ivar checker
in this nonisolated context in the init?
Please do not judge why I put a @MainActor
ivar in an actor, etc. This is just experimental code for me to learn swift concurrency.
Non-async initialisers in an actor are indeed not isolated to the actor, because of their synchronous nature - an actor hop cannot be done.
However, the compiler can prove that accessing self.checker
is safe in the initialiser. If two threads call init
at the same time, then there will just be two actor instances created, each with their own checker
.
During the execution of the initialiser, no other thread can access self
(the object that is being initialised) either, unless the initialiser passed self
to somewhere else.
Since checker
is main actor isolated, a good example of this is to try to pass self
to a @MainActor
function.
actor SomeActor {
@MainActor
var checker: @MainActor @Sendable () -> Void = {}
init(checker: @escaping @Sendable () -> Void) {
self.checker = checker
foo(self) // error!
self.checker = { /* something */ }
}
}
@MainActor
func foo(_ a: SomeActor) {
// now foo has access to 'self.checker' too!
a.checker = { /* something else */ }
}
Here the compiler correctly gives you an error. If the compiler had allowed this, self.checker = { /* something */ }
and a.checker = { /* something else */ }
would potentially run on different threads, and god knows what gets written to checker
in the end.
Another example is capturing self
in a Task
running on the main actor.
init(checker: @escaping @Sendable () -> Void) {
self.checker = checker
// comment out the below line and the error is gone
Task { @MainActor in self.checker = { /* something */ } }
print(self.checker) // error here
}
Similarly, it is not safe to call any isolated methods on self
in init
. Those methods may start new Task
s that would race with whatever init
is doing. See the link for more details.
For more info on how Swift determines whether self
has escaped, see the "initialisers with nonisolated self" section in SE-0327.