I found that this code compiles, which I didn't expect:
@MainActor
@propertyWrapper
struct Wrapped<T> {
var wrappedValue: T
}
class MyClass {
@Wrapped var string: String
init(string: String) {
self.string = string
}
func doSomething() {
print(string)
}
}
How can this be OK? The initializer and doSomething must both need to be on the main actor to interact with the property wrapper, but nothing in MyClass is marked @MainActor?
Well, it turns out when you try to use it, that actually, both the initializer and the method have been implicitly marked @MainActor:
await Task.detached {
MyClass(string: "hi").doSomething()
// ^ error: expression is 'async' but is not marked with 'await'
// note: calls to instance method 'doSomething()' from outside of its actor context are implicitly asynchronous
// note: calls to initializer 'init(string:)' from outside of its actor context are implicitly asynchronous
}.value
Adding the await fixes things and all the code compiles:
await Task.detached {
await MyClass(string: "hi").doSomething()
}.value
My question is, is this intentional? That spooky action-at-a-distance in my codebase can spread @MainActor like wildfire? And if it is intentional, is it desirable? Wouldn't it be better to ask me to mark these methods @MainActor myself, so I understand the implications of what I've done?
1 Like
It’s intentional. From https://github.com/apple/swift-evolution/blob/main/proposals/0316-global-actors.md#global-actor-inference
A struct or class containing a wrapped instance property with a global actor-qualified wrappedValue infers actor isolation from that property wrapper
There’s also a Hacking From Swift article that talks about it - https://www.hackingwithswift.com/quick-start/concurrency/understanding-how-global-actor-inference-works
any struct or class using a property wrapper with @MainActor for its wrapped value will automatically be @MainActor. This is what makes @StateObject and @ObservedObject convey main-actor-ness on SwiftUI views that use them – if you use either of those two property wrappers in a SwiftUI view, the whole view becomes @MainActor too.
3 Likes