not an exhaustive enumeration, but a few things come to mind â the parameterized form is more general since the caller gets to specify the actor to which the function should be isolated when invoked. it need not use the implicit value that comes from the default parameter macro expansion:
func withIsolation(actor: isolated (any Actor)? = #isolation) async { ... }
actor A {}
let a1 = A()
await withIsolation(actor: a1)
let a2 = A()
await withIsolation(actor: a2)
it also allows constraining callers to use a known-nonnull actor instance at compile time by changing the type signature of the isolated parameter:
func onlyCallableWithActor(actor: isolated (any Actor) = #isolation) {
print("called with actor: \(actor)")
}
@concurrent
func noActor() async {
await onlyCallableWithActor() // đ error: 'nil' is not compatible with expected argument type 'any Actor'
}
actor A {
func doit() {
// #isolation macro expands to `self` so no await required
onlyCallableWithActor() // â prints: called with actor: A
}
}
additionally, nonisolated(nonsending) may currently only be used with async functions, which, as the above illustrates, is not a limitation shared by the parameterized approach.
in short â no, at least not in general. in specific cases though you can probably get away with converting between them. e.g. if you're trying to replace a function with a default isolated parameter with a nonisolated(nonsending) version and existing callers don't pass the parameter explicitly then a conversion would probably work out okay in many cases[1].
one last caveat i'll mention because it has been on my mind recently: in the nonisolated(nonsending) form, there is only access to the implicit isolated parameter via the #isolation macro. the value derived from that macro is not itself an isolated parameter â it's a binding to an isolated parameter (or at least that seems to be how the compiler treats it at the moment). unfortunately this means you can end up with confusing behavior due to the way closure isolation inference works, like this:
// build _without_ the NonisolatedNonsendingByDefault feature enabled
nonisolated(nonsending)
func perform(_ work: () async -> Void) async {
await work()
}
func forward_param_isolation(isolation: isolated (any Actor)? = #isolation) async {
print("parameter isolation: \(isolation, default: "<nil>")") // Optional(Swift.MainActor)
await perform {
// capture of the isolated paramter makes this closure isolated
// to that same parameter's value
_ = isolation
let closureIsolation = #isolation
print("parameter closure isolation: \(closureIsolation, default: "<nil>")") // Optional(Swift.MainActor)
assert(isolation === closureIsolation) // â
}
}
nonisolated(nonsending)
func forward_implicit_isolation() async {
let isolation = #isolation
print("inherited isolation: \(isolation, default: "<nil>")") // Optional(Swift.MainActor)
await perform {
// there's no capture of an isolated parameter, and this closure
// isn't itself nonisolated(nonsending), so it's not isolated
let closureIsolation = #isolation
print("inherited closure isolation: \(closureIsolation, default: "<nil>")") // nil
// assert(isolation === closureIsolation) // đĨ
}
}
@MainActor
func test() async {
await forward_param_isolation()
await forward_implicit_isolation()
}
await test()
IIUC there are also ABI differences b/w the two, which if you care about that sort of thing would presumably matter if trying to change from one to the other. âŠī¸