Hello,
I found a compiler warning in Xcode 16.0 beta (16A5171c) that, I believe, goes against the spirit of sending
introduced with SE-0430 sending
parameter and result values.
In my understanding, it is nicer for users of an API in a function accepts a sending
closure instead of a @Sendable
closure:
// OK, but...
func acceptSendableClosure(completion: @escaping @Sendable () -> Void) { }
// ... this is better.
func acceptSendingClosure(completion: sending @escaping () -> Void) { }
The reason why it is better is that it's easier to provide a sending
closure than a @Sendable
one:
Demonstration
class NonSendable { }
func meh() {
// ⚠️ Capture of 'ns' with non-sendable type 'NonSendable' in a
// `@Sendable` closure; this is an error in the Swift 6 language mode
let ns = NonSendable()
acceptSendableClosure { print(ns) }
}
func yeah() {
// No warning!
let ns = NonSendable()
acceptSendingClosure { print(ns) }
}
To sum up: friends don't let friends declare good old completion blocks as @Sendable
. They should be sending
.
Unfortunately, sending
does not play well when called in the context of an actor-isolated function:
func acceptSendingClosure(completion: sending @escaping () -> Void) { }
func acceptSendableClosure(completion: @escaping @Sendable () -> Void) { }
func noWarning1() throws {
// OK, no warning
acceptSendingClosure { }
}
func noWarning2() throws {
// OK, no warning
acceptSendableClosure { }
}
@MainActor func noWarning3() throws {
// OK, no warning
acceptSendableClosure { }
}
@MainActor func createWarning() throws {
// ⚠️ Main actor-isolated value of type '() -> ()' passed as a strongly
// transferred parameter; later accesses could race; this is an error
// in the Swift 6 language mode
acceptSendingClosure { }
}
This last warning completely ruins the benefits of providing a sending
closure in the first place.
"Ruins"? Isn't this hyperbolic?
You decide. Meanwhile, acceptSendingClosure
can't be used in MainActor-isolated client code, which is (hum) pretty frequent. For example, it can not be tested without warning:
class MyTests: XCTestCase {
@MainActor // required for `waitForExpectations`
func testMyFunction() {
let expectation = expectation(description: "completion")
// ⚠️ Main actor-isolated ...
acceptSendingClosure {
expectation.fulfill()
}
waitForExpectations(timeout: 1)
}
}
I should add that DispatchQueue.async(execute:)
is expected to start accepting sending
closures eventually. This warning implies that DispatchQueue.async
would start emitting warnings whenever it is called from an isolated function. This sounds odd to me.
Should I open an issue?
Thanks in advance - cc @hborla and @Michael_Gottesman