Consider the following example:
final class X: Sendable {
func bar() {}
}
func doIt(_ block: () -> Void) {
block()
}
func foo() {
let x = X()
doIt { [weak x] in
Task { // error: passing closure as a 'sending' parameter risks causing data races between code in the current task and concurrent execution of the closure
x?.bar() // note: closure captures 'x' which is accessible to code in the current task
}
// not needed, just to check what is going on
x = nil // ok, confirms that x is mutable
}
}
It seems that x
is captured as a variable, which causes an error in isolation checking and allows x to be mutated.
Which kinda makes sense, because Swift does not allow weak let
:
weak let y = x // error: 'weak' must be a mutable variable, because it may change at runtime
But understanding this does not help me much. I did not want to be able to modify the capture and did not express any intention for that, so for practical purposes this is a false positive from the isolation checker.
There is a workaround for that - to wrap it into a struct:
struct WeakX {
weak var x: X?
}
func foo() {
let x = X()
let weakX = WeakX(x: x)
doIt {
Task {
weakX.x?.bar()
}
}
}
And compiler is quite happy with immutable structs containing weak references, even though they also "may change at runtime".
It is probably possible to mitigate this with some hackery specific for closure captures, but maybe it is worth to reconsider the whole ban on weak let
? WDYT?