Why is this not allowed in Swift 6 (Xcode 16 Beta 3)?
class NonSendable { }
actor MyActor {
func foo() {
let nonSendable = NonSendable()
for _ in 1...3 {
// ✅ Compiles fine
bar(nonSendable)
}
(1...3).forEach { _ in
// ❌ Sending 'nonSendable' risks causing data races
// 'self'-isolated 'nonSendable' is captured by a actor-isolated
// closure. actor-isolated uses in closure may race against later
// nonisolated uses
bar(nonSendable)
}
}
func bar(_: NonSendable) { }
}
Swift 5.10 was overly conservative regarding passing non-Sendable
types to different contexts. Specifically, we might create a non-Sendable
instance, and pass it to some other context, but not use it outside of that new context. Swift 6 (specifically SE-0414) has improved this. As WWDC 2024 video What’s new in Swift says:
To ensure safety, complete concurrency checking in Swift 5.10 banned passing all non-
Sendable
values across actor isolation boundaries. Swift 6 can recognize that it is safe to pass non-Sendable
values, in scenarios where they can no longer be referenced from their original isolation boundary.
So, as you noted, in Swift 6 (in Xcode 16 beta 3), you will get a warning with the following code:
In this case, though, it is the presence of the reference to nonSendable
in the for
-in
loop that affects its isolation region. E.g., remove that reference and the error goes away:
actor MyActor {
func foo() {
let nonSendable = NonSendable()
// for _ in 1...3 {
// bar(nonSendable)
// }
(1...3).forEach { _ in
// ✅ Compiles fine
bar(nonSendable)
}
}
func bar(_ object: NonSendable) { }
}
This Swift 6 behavior is an improvement over Swift 5.10. See SE-0414 – Region based Isolation for a lengthy discussion regarding what improvements Swift 6 provides and the limitations that are still imposed.
For the sake of clarity, your original example (with both the for
-in
loop and the forEach
closure) did not actually manifest a data race. But the question is whether the compiler can guarantee that the code is free from races: At this point it cannot.
In terms of work-arounds, either avoid attempting to use the nonSendable
instance from two different regions, or make the object Sendable
.
As of Xcode 16.2, neither Swift 5 or Swift 6 language modes will produce the error in the OP’s original code snippet. The compiler is now much smarter and can reason about the code snippet better, and realizes that there is no issue.
So, to illustrate the broader issue, here is a contemporary example that still illustrates how region based isolation will warn you of potential races. In the following, I can send the non-Sendable
object to a different asynchronous context, but only if there are no subsequent references in the original context. Consider:
actor MyActor {
func foo() {
let nonSendable = NonSendable()
Task.detached {
self.bar(nonSendable)
}
// if you uncomment the following, the above will warn you of the potential race
//
// print(nonSendable)
}
nonisolated func bar(_ object: NonSendable) { }
}