Having stared at this warning for a long time now, it seems to me to be a false positive, but maybe I’m missing something? Help me see it.
@MainActor type Foo has a lazy var property that is initialized (lazily) with an instance of @MainActor type Bar. Bar’s initializer takes a parameter of type Baz that is notSendable.
/* intentionally not sendable */
class Baz {
}
@MainActor
final class Bar {
init(baz: Baz) {}
}
@MainActor
final class Foo {
let baz: Baz
private lazy var myLazyProperty = Bar(baz: baz)
// warning: non-sendable type 'Baz' in asynchronous access to main actor-isolated property 'baz' cannot cross actor boundary
init(baz: Baz) {
self.baz = baz
}
}
The warning doesn’t make sense to me because:
The property baz is isolated to the main actor
The property myLazyProperty is also isolated to the main actor
So when myLazyProperty is lazily initialized, no actor boundaries are being crossed (right?)
Hoping to increase my understanding of what’s going on (or happy to file a compiler bug).
The error might a bit unclear, but it is correct in the terms of it introducing an issue. Consider following code:
// nonisolated async function
func test() async {
let baz = Baz()
let foo = Foo(baz: baz)
}
Foo is isolated on the main actor, baz are passed from a nonisolated region, which means when you are passing it to a Foo initializer it is crossing an isolation boundary first time. But being non-Sendable it cannot (at least for now - region based isolation will address this cases) safely cross isolation boundary. The (mostly) same thing happens for Bar with it receiving in general non-Sendable object.
There are actually more details with initialisation, which covered in this proposal:
Thank you for your reply, @vns. In the example you gave, I do see how baz crosses an isolation boundary (and how SE-0430 would address it). However, I think there is an important difference between your sample code and mine. In my sample code, baz is already isolated to the main actor at the point in time it is passed to the Bar initializer (which is also isolated to the main actor), and thus doesn’t cross a boundary (IIUC).
I should note that initializing the lazy var with an IIFE takes care of the warning, I just don’t understand the semantic difference between the two:
private lazy var myLazyProperty = { @MainActor in Bar(baz: baz) }()
// no warning
Thanks, @vns. I’ll read through SE-0327 again and see if I can figure out what I’m missing.
And this works exactly as direct consequence: your initialization of lazy property now isolated on the main actor and everything is fine.
My understanding is that my original version not using IIFE is also isolated on the main actor, so I don’t understand the difference. Because myLazyProperty is isolated to the main actor, it can only be accessed from the main actor, and thus any lazy initialization (on first access some time after init) will happen on the main actor.
Maybe my mental model of lazy is wrong, but shouldn’t myLazyProperty and a manually-implemented myLazyProperty2 below be the exact same (in terms of concurrency warnings)?
/* intentionally not sendable */
class Baz {
}
@MainActor
final class Bar {
init(baz: Baz) {}
}
@MainActor
final class Foo {
let baz: Baz
private lazy var myLazyProperty = Bar(baz: baz)
// ↳ warning: non-sendable type 'Baz' in asynchronous access to main actor-isolated property 'baz' cannot cross actor boundary
private var _myLazyProperty2: Optional<Bar> = .none
private var myLazyProperty2: Bar {
get {
switch _myLazyProperty2 {
case .none:
let bar = Bar(baz: baz)
_myLazyProperty2 = .some(bar)
return bar
case let .some(bar):
return bar
}
}
}
// ↳ no warnings
init(baz: Baz) {
self.baz = baz
}
}
It’s not easy as for me to get there everything from first take, I’ve gone through whole document few times And still feel I have to revisit it. The second link is to case you are running into with nonisolated self
Here I step on slippery ground, because most what I know that lazy variables is connected with object initialization — IIRC related to the memory. Probably have to go through that again to give a complete answer, and just for myself too. In general it is not exactly like the second version you’ve provided.