Mr. Wu already gave an excellent answer. However, I would like to elaborate further on why I think this is actually consistent (at least until recently, with the introduction of nonisolated(nonsending) ) with closure isolation rules in general. As you probably know, closures formed within an isolated context that have a notion of isolation (i.e., async closures) are nonisolated by default (except when the isolation is derived from a global actor):
func noIso() async {
let c = { () async -> Void in
print("noIso", #isolation as (any Actor)?)
}
await c() // nil
}
@MainActor func isoToMain() async {
let c = { () async -> Void in
print("isoToMain", #isolation as (any Actor)?)
}
await c() // MainActor
}
func isoToSomething(_ iso: isolated (any Actor)?) async {
let c = { () async -> Void in
print("isoToSomething", #isolation as (any Actor)?)
}
await c() // nil
}
As with Task, you can capture the context’s isolation to isolate the closure like this:
func captureIsoInClosure(_ iso: isolated (any Actor)?) async {
let c = { () async -> Void in
_ = iso
print("captureIsoInClosure", #isolation as (any Actor)?)
}
await c() // iso isolated
}
However, you need to capture the isolation directly, as aliasing doesn’t work:
func failedToCaptureIsoInClosure(_ iso: isolated (any Actor)?) async {
let c = { [iso] () async -> Void in
print("failedToCaptureIsoInClosure", #isolation as (any Actor)?)
}
await c() // nil
}
The difference between “normal closures” and “Task closures” is that the latter allows you to isolate even @Sendableclosures. An @Sendable closure normally represents a nonisolated task, i.e., the closure executes on the GCE:
actor A {
func doSomething() {}
func f() {
let c = { @Sendable () async -> Void in
self.doSomething() // cannot be called from outside of its isolation
}
Task {
self.doSomething() // Okay
}
}
}
Personally, I think this is expected behavior if you’re aware of the general rules of closure isolation. However, I also think it is overall terribly confusing and needs further improvement. SE-0461 was already a step in the right direction, as it got rid of SE-0338 (which in hindsight was a rather poorly thought-out change, in my opinion).
As for your other question, I think all four methods, that I can come-up with, have their place: #isolation, assertIsolated/preconditionIsolated, wrapping partial task in print statements, and examining the SIL.
P.S. You might find the Godbolt examples in this comment interesting: [Pre-Pitch]: updating `with{Checked|Unsafe}Continuation` to support typed throws (and perhaps nonisolated(nonsending)) - #8 by NotTheNHK