What is the rationale behind Swift compiler being overly strict about Protocol conformances? For instance, consider the following code:
protocol TestProtocol {
func testFunction()
}
actor TestClass: TestProtocol {
func testFunction() {
}
}
The build fails with the error saying Actor-isolated instance method 'testFunction()' cannot be used to satisfy nonisolated protocol requirement
.
But isn’t the whole purpose of actor
thread safety and preventing data races? If so, where is the data race in the above code or conformance to protocol in general?
Edit: Based on the answers received from @Sweeper and @paulw11, here is the code I wrote and it crashes at runtime with error Incorrect actor executor assumption
import Foundation
protocol TestProtocol {
func increment()
func decrement()
}
actor TestActor: @preconcurrency TestProtocol {
var i = 5
func increment() {
i = i + 1
print("incremented (i)")
}
func decrement() {
i = i - 1
print("decremented (i)")
}
}
class TestClass {
let testActor = TestActor()
func g(x: any TestProtocol) {
x.increment() // this synchronously calls an isolated method!
x.decrement()
}
func f() {
print("inside f")
g(x: testActor)
}
}
2
Answers
You can refer to protocols in your code with the concrete type being determined at runtime.
Consider:
The compiler is happy with this because it knows that any object that conforms to
TestProtocol
will implementtestFunction
and therefore everything will be fine at runtime.But functions in an
actor
need to be asynchronous, which means that they need to beawait
ed.If I was permitted to define
There would be a problem with
doSomething
– It isn’t expecting an asynchronous function.To prevent this, the compiler will not allow an actor to implement a non-isolated protocol.
You need your protocol to inherit from
Actor
:Once you do that, the compiler will tell you that
testFunction
needs to beasync
And now, the compiler knows that invocations of
testFunction
must be asynchronous:Normally, if you want to call an actor’s isolated method from a context not isolated to that actor instance, the call must be asynchronous (you need to write
await
). You are submitting a job to the serial executor, which the serial executor will execute when it’s become free. This is why this is an asynchronous call, and this is how an actor synchronises access to its state.Your code, if it were allowed, allows you to call an actor’s method synchronously!
Now imagine many different threads concurrently calling
g(x: instance)
, and thattestFunction
mutates some state that should have been protected by the actor. The actor cannot serialise these calls because they are not asynchronously waiting for the serial executor to become free, and you end up with data races.You can change the protocol to inherit from
Actor
:Now the compiler understands that
testFunction
must be a function isolated to some actor, so the isolatedtestFunction
inTestClass
can satisfy the protocol requirement. Calling it synchronously ing
will now produce an error.