Implicit "actor pollution"

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